diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 7261b3d..2d920dc 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -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: { @@ -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: { diff --git a/apps/mobile/src/__tests__/friend-detail.test.tsx b/apps/mobile/src/__tests__/friend-detail.test.tsx index 32ceeb6..76d81fe 100644 --- a/apps/mobile/src/__tests__/friend-detail.test.tsx +++ b/apps/mobile/src/__tests__/friend-detail.test.tsx @@ -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 } }) => ( + + {options?.title ?? ""} + + ), + }, + }; +}); jest.mock("../lib/trpc", () => ({ api: { @@ -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( + + + , + ); + 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( { 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, @@ -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( { 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 () => { diff --git a/apps/mobile/src/app/(tabs)/friends/[id].tsx b/apps/mobile/src/app/(tabs)/friends/[id].tsx index 4f56ca4..4293009 100644 --- a/apps/mobile/src/app/(tabs)/friends/[id].tsx +++ b/apps/mobile/src/app/(tabs)/friends/[id].tsx @@ -1,5 +1,7 @@ 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 { @@ -7,7 +9,6 @@ import { BalanceText, EmptyState, ErrorState, - Intro, ListRow, LoadingState, PrimaryButton, @@ -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(); @@ -68,8 +75,28 @@ export default function FriendDetailScreen() { const activityGroups = groupActivityByDate(activity); return ( <> - - + { + 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); + }} + > + { + const { y, height } = event.nativeEvent.layout; + identityBottomRef.current = y + height; + }} + style={{ alignItems: "center", gap: 10, paddingVertical: 8 }} + > - - Direct and group balances stay separated so every amount remains - traceable to its original ledger. - {summary?.balances.length ? (
{summary.balances.map((balance, index) => ( @@ -231,7 +254,7 @@ export default function FriendDetailScreen() { />
- + ); } diff --git a/apps/mobile/src/app/(tabs)/profile/_layout.tsx b/apps/mobile/src/app/(tabs)/profile/_layout.tsx index 230810e..a0d0b68 100644 --- a/apps/mobile/src/app/(tabs)/profile/_layout.tsx +++ b/apps/mobile/src/app/(tabs)/profile/_layout.tsx @@ -1,4 +1,5 @@ import { Stack } from "expo-router"; +import { Platform } from "react-native"; import { formSheetOptions, inlineLargeTitleOptions, @@ -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 ( diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 658ebe0..0204a23 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -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, @@ -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 ( diff --git a/apps/mobile/src/app/settlement/new.tsx b/apps/mobile/src/app/settlement/new.tsx index b94d022..cd183dc 100644 --- a/apps/mobile/src/app/settlement/new.tsx +++ b/apps/mobile/src/app/settlement/new.tsx @@ -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; @@ -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, @@ -723,6 +720,7 @@ export default function NewSettlementScreen() { <> { + const nextWidth = event.nativeEvent.layout.width; + setWidth((current) => current === nextWidth ? current : nextWidth); + }} style={{ - width, + width: "100%", + maxWidth: 768, height: 76, overflow: "hidden", borderRadius: 24, @@ -72,7 +77,7 @@ export function AllocationFloatingSummary({ : "0 10px 28px rgba(0, 0, 0, 0.18)", }} > - 0 ? @@ -115,7 +120,7 @@ export function AllocationFloatingSummary({ ]} /> - + : null} ); } diff --git a/apps/mobile/src/components/allocation-floating-summary.tsx b/apps/mobile/src/components/allocation-floating-summary.tsx index ed612d0..6609c7b 100644 --- a/apps/mobile/src/components/allocation-floating-summary.tsx +++ b/apps/mobile/src/components/allocation-floating-summary.tsx @@ -1,4 +1,4 @@ -import { Text, useWindowDimensions, View } from "react-native"; +import { Text, View } from "react-native"; import { useTheme } from "../theme"; /** @@ -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; @@ -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, diff --git a/apps/mobile/src/components/expense-allocation-ui.tsx b/apps/mobile/src/components/expense-allocation-ui.tsx index 25aa54c..b68fa56 100644 --- a/apps/mobile/src/components/expense-allocation-ui.tsx +++ b/apps/mobile/src/components/expense-allocation-ui.tsx @@ -175,7 +175,9 @@ export function InlineAmountInput({ const digitCount = value.replace(/\D/g, "").length; const fontSize = digitCount > 9 ? 13 : digitCount > 6 ? 14 : 15; return ( - inputRef.current?.focus()} style={{ width, minHeight: 34, @@ -196,7 +198,6 @@ export function InlineAmountInput({ onFocus={() => onFocus?.(inputRef.current)} onBlur={() => onBlur?.(inputRef.current)} keyboardType={keyboardType} - returnKeyType="done" selectTextOnFocus numberOfLines={1} placeholder={placeholder} @@ -221,7 +222,7 @@ export function InlineAmountInput({ > {suffix} - + ); } diff --git a/apps/mobile/src/components/expense-composer-ui.tsx b/apps/mobile/src/components/expense-composer-ui.tsx index b03c42a..53fc1b2 100644 --- a/apps/mobile/src/components/expense-composer-ui.tsx +++ b/apps/mobile/src/components/expense-composer-ui.tsx @@ -3,7 +3,6 @@ import { Pressable, Text, TextInput, - useWindowDimensions, View, type ColorValue, } from "react-native"; @@ -339,12 +338,11 @@ export function ExpenseSaveControl({ foregroundColor?: ColorValue; }) { const theme = useTheme(); - const { width } = useWindowDimensions(); - return ( { 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(); expect(view.getByTestId("screen-bottom-overlay")).toBeTruthy(); expect(view.getByText("Save expense")).toBeTruthy(); expect(view.getAllByText("Add expense")).toHaveLength(2); diff --git a/apps/mobile/src/components/expense-editor.tsx b/apps/mobile/src/components/expense-editor.tsx index b11a362..0cd3ee8 100644 --- a/apps/mobile/src/components/expense-editor.tsx +++ b/apps/mobile/src/components/expense-editor.tsx @@ -232,7 +232,7 @@ export function ExpenseEditor({ keyboardClearance, focusInput: focusBottomInput, blurInput: blurBottomInput, - revealFocusedInput: revealBottomInput, + onScroll: onBottomScroll, } = useKeyboardFocusScroll(screenRef, expenseOverlayHeight + 16); const detectedIconKey = useMemo( () => detectExpenseIconKey(description), @@ -736,6 +736,7 @@ export function ExpenseEditor({ <> focusBottomInput(notesInputRef.current)} onBlur={() => blurBottomInput(notesInputRef.current)} - onContentSizeChange={() => - requestAnimationFrame(revealBottomInput) - } textAlignVertical="top" style={{ color: theme.text, diff --git a/apps/mobile/src/components/expense-item-split-editor.tsx b/apps/mobile/src/components/expense-item-split-editor.tsx index 2e0a314..5f79c86 100644 --- a/apps/mobile/src/components/expense-item-split-editor.tsx +++ b/apps/mobile/src/components/expense-item-split-editor.tsx @@ -66,7 +66,7 @@ export function ExpenseItemSplitEditor() { request?.item, ); const screenRef = useRef(null); - const { keyboardClearance, focusInput, blurInput } = + const { keyboardClearance, focusInput, blurInput, onScroll } = useKeyboardFocusScroll(screenRef, 104); useEffect(() => { @@ -150,6 +150,7 @@ export function ExpenseItemSplitEditor() { - {process.env.EXPO_OS === "ios" ? "Done" : null} + {process.env.EXPO_OS === "ios" ? "Save" : null} diff --git a/apps/mobile/src/components/expense-payment-editor.tsx b/apps/mobile/src/components/expense-payment-editor.tsx index 73fcecd..9a5bda3 100644 --- a/apps/mobile/src/components/expense-payment-editor.tsx +++ b/apps/mobile/src/components/expense-payment-editor.tsx @@ -37,7 +37,7 @@ export function ExpensePaymentEditor() { request?.draft, ); const screenRef = useRef(null); - const { keyboardClearance, focusInput, blurInput } = + const { keyboardClearance, focusInput, blurInput, onScroll } = useKeyboardFocusScroll(screenRef, 104); useEffect(() => { @@ -107,6 +107,7 @@ export function ExpensePaymentEditor() { <> - {process.env.EXPO_OS === "ios" ? "Done" : null} + {process.env.EXPO_OS === "ios" ? "Save" : null} diff --git a/apps/mobile/src/components/expense-split-editor.test.tsx b/apps/mobile/src/components/expense-split-editor.test.tsx index e97649e..07dbdb8 100644 --- a/apps/mobile/src/components/expense-split-editor.test.tsx +++ b/apps/mobile/src/components/expense-split-editor.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render } from "@testing-library/react-native"; import type { ReactNode } from "react"; -import { Text, Pressable } from "react-native"; +import { Text, Pressable, StyleSheet } from "react-native"; import { SafeAreaInsetsContext } from "react-native-safe-area-context"; import { createExpenseSplitDraft, @@ -120,6 +120,13 @@ describe("ExpenseSplitEditor", () => { wrapper: Wrapper, }); await fireEvent.press(view.getByText("Open editor")); + await fireEvent(view.getByTestId("split-method-section"), "layout", { + nativeEvent: { layout: { x: 0, y: 0, width: 720, height: 120 } }, + }); + expect( + StyleSheet.flatten(view.getByTestId("split-method-picker").props.style) + .width, + ).toBe(720); await fireEvent(view.getByTestId("split-method-picker"), "pressAction", { nativeEvent: { event: "percentage" }, }); diff --git a/apps/mobile/src/components/expense-split-editor.tsx b/apps/mobile/src/components/expense-split-editor.tsx index e3018e0..0077349 100644 --- a/apps/mobile/src/components/expense-split-editor.tsx +++ b/apps/mobile/src/components/expense-split-editor.tsx @@ -58,13 +58,14 @@ export function ExpenseSplitEditor() { const [draft, setDraft] = useState( request?.draft, ); + const [methodWidth, setMethodWidth] = useState(0); const screenRef = useRef(null); const itemInputRefs = useRef(new Map()); const { keyboardClearance, focusInput, blurInput, - revealFocusedInput, + onScroll, } = useKeyboardFocusScroll(screenRef, 104); useEffect(() => { if (request) setDraft(request.draft); @@ -267,6 +268,7 @@ export function ExpenseSplitEditor() { <> - + { + const width = event.nativeEvent.layout.width; + setMethodWidth((current) => current === width ? current : width); + }} + > 0 ? { width: methodWidth } : { width: "100%" }} onPressAction={({ nativeEvent }) => { const nextMode = nativeEvent.event; if (!isExpenseSplitMode(nextMode)) return; @@ -296,6 +306,7 @@ export function ExpenseSplitEditor() { 0 ? methodWidth : "100%", minHeight: 68, paddingHorizontal: 16, paddingVertical: 12, @@ -590,9 +601,6 @@ export function ExpenseSplitEditor() { itemInputRefs.current.get(`${item.id}:name`) ?? null, ) } - onContentSizeChange={() => - requestAnimationFrame(revealFocusedInput) - } placeholder="Item name (optional)" placeholderTextColor={theme.subtle} selectionColor={theme.primary} @@ -908,7 +916,7 @@ export function ExpenseSplitEditor() { disabled={!status.valid} onPress={done} > - {process.env.EXPO_OS === "ios" ? "Done" : null} + {process.env.EXPO_OS === "ios" ? "Save" : null} diff --git a/apps/mobile/src/components/floating-controls.test.tsx b/apps/mobile/src/components/floating-controls.test.tsx new file mode 100644 index 0000000..5b1a718 --- /dev/null +++ b/apps/mobile/src/components/floating-controls.test.tsx @@ -0,0 +1,34 @@ +import { render } from "@testing-library/react-native"; +import { StyleSheet } from "react-native"; +import { ExpenseSaveControl } from "./expense-composer-ui"; +import { SettlementSaveControl } from "./settlement-composer"; +import { AllocationFloatingSummary } from "./allocation-floating-summary"; + +describe("floating controls in form sheets", () => { + it("sizes save controls to the sheet instead of the device window", async () => { + for (const Control of [ExpenseSaveControl, SettlementSaveControl]) { + const view = await render( + , + ); + const [container] = view.container.queryAll((instance) => { + const style = StyleSheet.flatten(instance.props.style); + return style?.maxWidth === 768; + }); + const style = StyleSheet.flatten(container?.props.style); + expect(style.width).toBe("100%"); + expect(style.maxWidth).toBe(768); + await view.unmount(); + } + }); + + it("keeps the allocation summary inside the sheet", async () => { + const view = await render( + , + ); + const style = StyleSheet.flatten( + view.getByTestId("allocation-floating-summary").props.style, + ); + expect(style.width).toBe("100%"); + expect(style.maxWidth).toBe(768); + }); +}); diff --git a/apps/mobile/src/components/group-statistics.tsx b/apps/mobile/src/components/group-statistics.tsx index fa5ba70..b3078a7 100644 --- a/apps/mobile/src/components/group-statistics.tsx +++ b/apps/mobile/src/components/group-statistics.tsx @@ -564,7 +564,10 @@ function TimelineChart({ const highlighted = selected ?? latest; const columnWidth = Math.max( 44, - Math.floor((width - 80) / Math.max(1, Math.min(chartData.length, 8))), + Math.floor( + ((process.env.EXPO_OS === "ios" ? Math.min(width, 800) : width) - 80) / + Math.max(1, Math.min(chartData.length, 8)), + ), ); const timelineKey = `${rangeKey}:${latest?.period ?? "empty"}`; const scrollToLatest = useCallback(() => { diff --git a/apps/mobile/src/components/settlement-composer.tsx b/apps/mobile/src/components/settlement-composer.tsx index db72709..b2d0b0d 100644 --- a/apps/mobile/src/components/settlement-composer.tsx +++ b/apps/mobile/src/components/settlement-composer.tsx @@ -3,12 +3,11 @@ import { MenuView, type MenuAction, } from "@expo/ui/community/menu"; -import { useRef, type ReactNode } from "react"; +import { useRef, useState, type ReactNode } from "react"; import { Pressable, Text, TextInput, - useWindowDimensions, View, } from "react-native"; import { useTheme } from "../theme"; @@ -30,6 +29,7 @@ function PartyChoice({ onValueChange, disabled, testID, + width, }: { label: string; value: SettlementMember | undefined; @@ -38,6 +38,7 @@ function PartyChoice({ onValueChange: (userId: string) => void; disabled: boolean; testID: string; + width: number; }) { const theme = useTheme(); const name = value @@ -51,7 +52,7 @@ function PartyChoice({ accessibilityRole={disabled ? undefined : "button"} accessibilityLabel={`${label}: ${name}`} style={{ - width: "100%", + width: width > 0 ? width : "100%", minWidth: 0, alignItems: "center", gap: 7, @@ -94,7 +95,7 @@ function PartyChoice({ {name} @@ -119,7 +120,7 @@ function PartyChoice({ title={label} actions={actions} testID={testID} - style={{ width: "100%" }} + style={width > 0 ? { width } : { width: "100%" }} onPressAction={({ nativeEvent }) => onValueChange(nativeEvent.event)} > {content} @@ -145,6 +146,7 @@ export function SettlementDirectionCard({ locked: boolean; }) { const theme = useTheme(); + const [slotWidth, setSlotWidth] = useState(0); return ( - + { + const width = event.nativeEvent.layout.width; + setSlotWidth((current) => current === width ? current : width); + }} + > - + @@ -326,11 +337,11 @@ export function SettlementSaveControl({ disabled: boolean; }) { const theme = useTheme(); - const { width } = useWindowDimensions(); return ( { + it("scrolls only when the focused input is covered by the keyboard", async () => { + const scrollTo = jest.fn(); + const inputY = { current: 100 }; + const input = { + measureInWindow: (callback: (x: number, y: number, width: number, height: number) => void) => + callback(0, inputY.current, 100, 34), + } as unknown as TextInput; + const scrollView = { + getNativeScrollRef: () => ({ + measureInWindow: (callback: (x: number, y: number, width: number, height: number) => void) => + callback(0, 0, 400, 700), + }), + scrollTo, + } as unknown as ScrollView; + const scrollRef = { current: scrollView }; + let controls!: ReturnType; + function Probe() { + controls = useKeyboardFocusScroll(scrollRef, 104); + return null; + } + const metrics = jest.spyOn(Keyboard, "metrics").mockReturnValue({ + screenX: 0, + screenY: 500, + width: 400, + height: 300, + }); + try { + await render(); + await act(async () => { + controls.focusInput(input); + controls.revealFocusedInput(); + }); + expect(scrollTo).not.toHaveBeenCalled(); + + inputY.current = 450; + controls.onScroll({ nativeEvent: { contentOffset: { y: 20 } } } as Parameters[0]); + await act(async () => controls.revealFocusedInput()); + expect(scrollTo).toHaveBeenCalledWith({ y: 108, animated: true }); + } finally { + metrics.mockRestore(); + } + }); +}); + describe("EmptyState", () => { it("renders an accessible title and explanation", async () => { const view = await render( diff --git a/apps/mobile/src/components/ui.tsx b/apps/mobile/src/components/ui.tsx index dfcef22..c3aff0a 100644 --- a/apps/mobile/src/components/ui.tsx +++ b/apps/mobile/src/components/ui.tsx @@ -15,7 +15,6 @@ import { } from "react"; import { ActivityIndicator, - findNodeHandle, Keyboard, Pressable, RefreshControl, @@ -24,6 +23,7 @@ import { Text, TextInput, useColorScheme, + useWindowDimensions, View, type AccessibilityState, type ColorValue, @@ -159,53 +159,74 @@ export function useKeyboardFocusScroll( additionalOffset = spacing.md, ) { const focusedInputRef = useRef(null); + const scrollYRef = useRef(0); + const keyboardTopRef = useRef(null); const [keyboardClearance, setKeyboardClearance] = useState(0); const revealFocusedInput = useCallback(() => { - const inputHandle = findNodeHandle(focusedInputRef.current); - if (inputHandle === null) return; + const input = focusedInputRef.current; + const keyboardTop = keyboardTopRef.current; + if (!input || keyboardTop === null) return; const scrollView = scrollViewRef.current; if (!scrollView) return; - const reveal = (viewportScreenY: number) => { - scrollView.scrollResponderScrollNativeHandleToKeyboard( - inputHandle, - additionalOffset + viewportScreenY, - true, - ); - }; const nativeScrollView = scrollView.getNativeScrollRef(); - if (!nativeScrollView) { - reveal(0); - return; - } - nativeScrollView.measureInWindow((_x, y) => reveal(y)); + if (!nativeScrollView) return; + nativeScrollView.measureInWindow((_x, viewportY, _width, viewportHeight) => { + input.measureInWindow((_inputX, inputY, _inputWidth, inputHeight) => { + const visibleTop = viewportY + spacing.md; + const visibleBottom = Math.min( + viewportY + viewportHeight, + keyboardTop, + ) - additionalOffset; + const inputBottom = inputY + inputHeight; + const delta = + inputBottom > visibleBottom + ? inputBottom - visibleBottom + : inputY < visibleTop + ? inputY - visibleTop + : 0; + if (Math.abs(delta) <= 1) return; + scrollView.scrollTo({ + y: Math.max(0, scrollYRef.current + delta), + animated: true, + }); + }); + }); }, [additionalOffset, scrollViewRef]); useEffect(() => { const shown = Keyboard.addListener("keyboardDidShow", (event) => { if (!focusedInputRef.current) return; + keyboardTopRef.current = event.endCoordinates.screenY; + setKeyboardClearance(event.endCoordinates.height); + requestAnimationFrame(revealFocusedInput); + }); + const changed = Keyboard.addListener("keyboardDidChangeFrame", (event) => { + if (!focusedInputRef.current) return; + keyboardTopRef.current = event.endCoordinates.screenY; setKeyboardClearance(event.endCoordinates.height); requestAnimationFrame(revealFocusedInput); }); const hidden = Keyboard.addListener("keyboardDidHide", () => { + keyboardTopRef.current = null; setKeyboardClearance(0); }); return () => { shown.remove(); + changed.remove(); hidden.remove(); }; }, [revealFocusedInput]); - useEffect(() => { - if (keyboardClearance <= 0) return; - requestAnimationFrame(revealFocusedInput); - }, [keyboardClearance, revealFocusedInput]); - const focusInput = useCallback( (input: TextInput | null) => { focusedInputRef.current = input; - setKeyboardClearance(Keyboard.metrics()?.height ?? 0); - requestAnimationFrame(revealFocusedInput); + const metrics = Keyboard.metrics(); + if (metrics?.height) { + keyboardTopRef.current = metrics.screenY; + setKeyboardClearance(metrics.height); + requestAnimationFrame(revealFocusedInput); + } }, [revealFocusedInput], ); @@ -219,6 +240,9 @@ export function useKeyboardFocusScroll( focusInput, blurInput, revealFocusedInput, + onScroll: (event: Parameters>[0]) => { + scrollYRef.current = event.nativeEvent.contentOffset.y; + }, }; } @@ -255,6 +279,11 @@ export function Screen({ onScroll?: ScrollViewProps["onScroll"]; }>) { const theme = useTheme(); + const { width: windowWidth } = useWindowDimensions(); + const tabletContentStyle = + process.env.EXPO_OS === "ios" && windowWidth >= 700 + ? styles.tabletContent + : null; const insets = useSafeAreaInsets(); const keyboardBottomClearance = useKeyboardBottomClearance(); const backgroundColor = @@ -282,6 +311,7 @@ export function Screen({ ? { paddingBottom: bottomOverlayHeight + insets.bottom } : null, contentContainerStyle, + tabletContentStyle, ]} > {children} @@ -303,6 +333,7 @@ export function Screen({ styles.screenContent, fillStyle, contentContainerStyle, + tabletContentStyle, ]} contentInsetAdjustmentBehavior="automatic" keyboardDismissMode="interactive" @@ -369,6 +400,7 @@ export function CollectionScreen({ onRefresh?: () => void; }>) { const theme = useTheme(); + const { width: windowWidth } = useWindowDimensions(); const { fillStyle, onLayout, onContentSizeChange } = useScrollViewportFill({ underlapsHeader: true, }); @@ -379,6 +411,9 @@ export function CollectionScreen({ styles.collectionContent, fillStyle, isEmpty ? styles.emptyCollectionContent : null, + process.env.EXPO_OS === "ios" && windowWidth >= 700 + ? styles.tabletContent + : null, ]} contentInsetAdjustmentBehavior="automatic" alwaysBounceVertical={!isEmpty || Boolean(onRefresh)} @@ -1055,6 +1090,11 @@ export function SelectionPill({ const styles = StyleSheet.create({ screen: { flex: 1 }, + tabletContent: { + width: "100%", + maxWidth: 800, + alignSelf: "center", + }, collectionContent: { paddingHorizontal: spacing.md, paddingTop: spacing.sm, diff --git a/packages/db/drizzle/0013_gray_thunderbolts.sql b/packages/db/drizzle/0013_gray_thunderbolts.sql new file mode 100644 index 0000000..087434a --- /dev/null +++ b/packages/db/drizzle/0013_gray_thunderbolts.sql @@ -0,0 +1,3 @@ +DROP INDEX "account_issuer_unique";--> statement-breakpoint +ALTER TABLE "account" ALTER COLUMN "issuer" DROP NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "account_provider_unique" ON "account" USING btree ("provider_id","account_id"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0013_snapshot.json b/packages/db/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..60fdc13 --- /dev/null +++ b/packages/db/drizzle/meta/0013_snapshot.json @@ -0,0 +1,2821 @@ +{ + "id": "63508c8c-f7f0-46ef-b1a6-954ae5fda4e2", + "prevId": "52f8262c-45b6-409a-a52c-da9e5f4f8076", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user_idx": { + "name": "account_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_provider_unique": { + "name": "account_provider_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.currency_quote": { + "name": "currency_quote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base": { + "name": "base", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rates": { + "name": "rates", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "currency_quote_user_idx": { + "name": "currency_quote_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "currency_quote_expiry_idx": { + "name": "currency_quote_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "currency_quote_user_id_user_id_fk": { + "name": "currency_quote_user_id_user_id_fk", + "tableFrom": "currency_quote", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.expense_payment": { + "name": "expense_payment", + "schema": "", + "columns": { + "expense_id": { + "name": "expense_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_amount_minor": { + "name": "source_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "expense_payment_user_idx": { + "name": "expense_payment_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "expense_payment_expense_id_expense_id_fk": { + "name": "expense_payment_expense_id_expense_id_fk", + "tableFrom": "expense_payment", + "tableTo": "expense", + "columnsFrom": [ + "expense_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expense_payment_user_id_user_id_fk": { + "name": "expense_payment_user_id_user_id_fk", + "tableFrom": "expense_payment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "expense_payment_expense_id_user_id_pk": { + "name": "expense_payment_expense_id_user_id_pk", + "columns": [ + "expense_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.expense_split": { + "name": "expense_split", + "schema": "", + "columns": { + "expense_id": { + "name": "expense_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_amount_minor": { + "name": "source_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "expense_split_user_idx": { + "name": "expense_split_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "expense_split_expense_id_expense_id_fk": { + "name": "expense_split_expense_id_expense_id_fk", + "tableFrom": "expense_split", + "tableTo": "expense", + "columnsFrom": [ + "expense_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expense_split_user_id_user_id_fk": { + "name": "expense_split_user_id_user_id_fk", + "tableFrom": "expense_split", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "expense_split_expense_id_user_id_pk": { + "name": "expense_split_expense_id_user_id_pk", + "columns": [ + "expense_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.expense": { + "name": "expense", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "context_type": { + "name": "context_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "friendship_id": { + "name": "friendship_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon_key": { + "name": "icon_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "icon_manually_set": { + "name": "icon_manually_set", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source_currency": { + "name": "source_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_amount_minor": { + "name": "source_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "expense_idempotency_unique": { + "name": "expense_idempotency_unique", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_mutation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_group_idx": { + "name": "expense_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_friendship_idx": { + "name": "expense_friendship_idx", + "columns": [ + { + "expression": "friendship_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_active_group_idx": { + "name": "expense_active_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_active_friendship_idx": { + "name": "expense_active_friendship_idx", + "columns": [ + { + "expression": "friendship_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "expense_group_id_expense_group_id_fk": { + "name": "expense_group_id_expense_group_id_fk", + "tableFrom": "expense", + "tableTo": "expense_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expense_friendship_id_friendship_id_fk": { + "name": "expense_friendship_id_friendship_id_fk", + "tableFrom": "expense", + "tableTo": "friendship", + "columnsFrom": [ + "friendship_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expense_created_by_user_id_fk": { + "name": "expense_created_by_user_id_fk", + "tableFrom": "expense", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expense_payer_id_user_id_fk": { + "name": "expense_payer_id_user_id_fk", + "tableFrom": "expense", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.financial_revision": { + "name": "financial_revision", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "financial_revision_version_unique": { + "name": "financial_revision_version_unique", + "columns": [ + { + "expression": "record_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "financial_revision_actor_id_user_id_fk": { + "name": "financial_revision_actor_id_user_id_fk", + "tableFrom": "financial_revision", + "tableTo": "user", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friendship": { + "name": "friendship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_low_id": { + "name": "user_low_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_high_id": { + "name": "user_high_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_via": { + "name": "created_via", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "friendship_pair_unique": { + "name": "friendship_pair_unique", + "columns": [ + { + "expression": "user_low_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_high_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "friendship_low_idx": { + "name": "friendship_low_idx", + "columns": [ + { + "expression": "user_low_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "friendship_high_idx": { + "name": "friendship_high_idx", + "columns": [ + { + "expression": "user_high_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "friendship_active_low_idx": { + "name": "friendship_active_low_idx", + "columns": [ + { + "expression": "user_low_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "removed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "friendship_active_high_idx": { + "name": "friendship_active_high_idx", + "columns": [ + { + "expression": "user_high_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "removed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friendship_user_low_id_user_id_fk": { + "name": "friendship_user_low_id_user_id_fk", + "tableFrom": "friendship", + "tableTo": "user", + "columnsFrom": [ + "user_low_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friendship_user_high_id_user_id_fk": { + "name": "friendship_user_high_id_user_id_fk", + "tableFrom": "friendship", + "tableTo": "user", + "columnsFrom": [ + "user_high_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_member": { + "name": "group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "group_member_user_idx": { + "name": "group_member_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "group_member_active_user_idx": { + "name": "group_member_active_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "removed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_member_group_id_expense_group_id_fk": { + "name": "group_member_group_id_expense_group_id_fk", + "tableFrom": "group_member", + "tableTo": "expense_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "group_member_user_id_user_id_fk": { + "name": "group_member_user_id_user_id_fk", + "tableFrom": "group_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_member_group_id_user_id_pk": { + "name": "group_member_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_notification_preference": { + "name": "group_notification_preference", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "group_notification_preference_user_idx": { + "name": "group_notification_preference_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_notification_preference_group_id_expense_group_id_fk": { + "name": "group_notification_preference_group_id_expense_group_id_fk", + "tableFrom": "group_notification_preference", + "tableTo": "expense_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_notification_preference_user_id_user_id_fk": { + "name": "group_notification_preference_user_id_user_id_fk", + "tableFrom": "group_notification_preference", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_notification_preference_group_id_user_id_pk": { + "name": "group_notification_preference_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.expense_group": { + "name": "expense_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon_key": { + "name": "icon_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#4745B8'" + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "simplify_debts": { + "name": "simplify_debts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "expense_group_created_by_user_id_fk": { + "name": "expense_group_created_by_user_id_fk", + "tableFrom": "expense_group", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite": { + "name": "invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_group_idx": { + "name": "invite_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_inviter_idx": { + "name": "invite_inviter_idx", + "columns": [ + { + "expression": "inviter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_group_id_expense_group_id_fk": { + "name": "invite_group_id_expense_group_id_fk", + "tableFrom": "invite", + "tableTo": "expense_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invite_inviter_id_user_id_fk": { + "name": "invite_inviter_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_entry": { + "name": "ledger_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context_type": { + "name": "context_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "debtor_id": { + "name": "debtor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creditor_id": { + "name": "creditor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_currency": { + "name": "canonical_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_amount_minor": { + "name": "canonical_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "reversal_of_id": { + "name": "reversal_of_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_source_idx": { + "name": "ledger_source_idx", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_context_idx": { + "name": "ledger_context_idx", + "columns": [ + { + "expression": "context_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "context_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_debtor_idx": { + "name": "ledger_debtor_idx", + "columns": [ + { + "expression": "debtor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_creditor_idx": { + "name": "ledger_creditor_idx", + "columns": [ + { + "expression": "creditor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_entry_debtor_id_user_id_fk": { + "name": "ledger_entry_debtor_id_user_id_fk", + "tableFrom": "ledger_entry", + "tableTo": "user", + "columnsFrom": [ + "debtor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ledger_entry_creditor_id_user_id_fk": { + "name": "ledger_entry_creditor_id_user_id_fk", + "tableFrom": "ledger_entry", + "tableTo": "user", + "columnsFrom": [ + "creditor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_valuation": { + "name": "ledger_valuation", + "schema": "", + "columns": { + "ledger_entry_id": { + "name": "ledger_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ledger_valuation_user_idx": { + "name": "ledger_valuation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_valuation_ledger_entry_id_ledger_entry_id_fk": { + "name": "ledger_valuation_ledger_entry_id_ledger_entry_id_fk", + "tableFrom": "ledger_valuation", + "tableTo": "ledger_entry", + "columnsFrom": [ + "ledger_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ledger_valuation_user_id_user_id_fk": { + "name": "ledger_valuation_user_id_user_id_fk", + "tableFrom": "ledger_valuation", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ledger_valuation_ledger_entry_id_user_id_pk": { + "name": "ledger_valuation_ledger_entry_id_user_id_pk", + "columns": [ + "ledger_entry_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_outbox": { + "name": "notification_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_mode": { + "name": "delivery_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'immediate'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notification_outbox_event_unique": { + "name": "notification_outbox_event_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_outbox_pending_idx": { + "name": "notification_outbox_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_outbox_installation_idx": { + "name": "notification_outbox_installation_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_outbox_installation_id_push_installation_id_fk": { + "name": "notification_outbox_installation_id_push_installation_id_fk", + "tableFrom": "notification_outbox", + "tableTo": "push_installation", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_outbox_recipient_user_id_user_id_fk": { + "name": "notification_outbox_recipient_user_id_user_id_fk", + "tableFrom": "notification_outbox", + "tableTo": "user", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "home_currency": { + "name": "home_currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'EUR'" + }, + "notifications_enabled": { + "name": "notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notification_only_when_involved": { + "name": "notification_only_when_involved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "summarize_notification_bursts": { + "name": "summarize_notification_bursts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarded_at": { + "name": "onboarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_installation": { + "name": "push_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_installation_token_unique": { + "name": "push_installation_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_installation_user_idx": { + "name": "push_installation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_installation_delivery_idx": { + "name": "push_installation_delivery_idx", + "columns": [ + { + "expression": "environment", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "disabled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_installation_user_id_user_id_fk": { + "name": "push_installation_user_id_user_id_fk", + "tableFrom": "push_installation", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_snapshot": { + "name": "rate_snapshot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expense_id": { + "name": "expense_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "settlement_id": { + "name": "settlement_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base": { + "name": "base", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quote": { + "name": "quote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric(30, 15)", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_date": { + "name": "provider_date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rate_expense_idx": { + "name": "rate_expense_idx", + "columns": [ + { + "expression": "expense_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rate_settlement_idx": { + "name": "rate_settlement_idx", + "columns": [ + { + "expression": "settlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rate_snapshot_expense_id_expense_id_fk": { + "name": "rate_snapshot_expense_id_expense_id_fk", + "tableFrom": "rate_snapshot", + "tableTo": "expense", + "columnsFrom": [ + "expense_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rate_snapshot_settlement_id_settlement_id_fk": { + "name": "rate_snapshot_settlement_id_settlement_id_fk", + "tableFrom": "rate_snapshot", + "tableTo": "settlement", + "columnsFrom": [ + "settlement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_user_idx": { + "name": "session_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settlement": { + "name": "settlement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "context_type": { + "name": "context_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "friendship_id": { + "name": "friendship_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_user_id": { + "name": "from_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_user_id": { + "name": "to_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source_currency": { + "name": "source_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_amount_minor": { + "name": "source_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "canonical_currency": { + "name": "canonical_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_amount_minor": { + "name": "canonical_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_mutation_id": { + "name": "client_mutation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "settlement_idempotency_unique": { + "name": "settlement_idempotency_unique", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_mutation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "settlement_group_idx": { + "name": "settlement_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "settlement_friendship_idx": { + "name": "settlement_friendship_idx", + "columns": [ + { + "expression": "friendship_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "settlement_active_group_idx": { + "name": "settlement_active_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "settlement_active_friendship_idx": { + "name": "settlement_active_friendship_idx", + "columns": [ + { + "expression": "friendship_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "settlement_group_id_expense_group_id_fk": { + "name": "settlement_group_id_expense_group_id_fk", + "tableFrom": "settlement", + "tableTo": "expense_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "settlement_friendship_id_friendship_id_fk": { + "name": "settlement_friendship_id_friendship_id_fk", + "tableFrom": "settlement", + "tableTo": "friendship", + "columnsFrom": [ + "friendship_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "settlement_created_by_user_id_fk": { + "name": "settlement_created_by_user_id_fk", + "tableFrom": "settlement", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "settlement_from_user_id_user_id_fk": { + "name": "settlement_from_user_id_user_id_fk", + "tableFrom": "settlement", + "tableTo": "user", + "columnsFrom": [ + "from_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "settlement_to_user_id_user_id_fk": { + "name": "settlement_to_user_id_user_id_fk", + "tableFrom": "settlement", + "tableTo": "user", + "columnsFrom": [ + "to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 5e64110..379376c 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1788446448403, "tag": "0012_flashy_jasper_sitwell", "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1790243657251, + "tag": "0013_gray_thunderbolts", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 333cb35..96b5135 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -79,7 +79,7 @@ export const accounts = pgTable( "account", { id: text("id").primaryKey(), - issuer: text("issuer").notNull(), + issuer: text("issuer"), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") @@ -100,7 +100,7 @@ export const accounts = pgTable( }, (table) => [ index("account_user_idx").on(table.userId), - uniqueIndex("account_issuer_unique").on(table.issuer, table.accountId), + uniqueIndex("account_provider_unique").on(table.providerId, table.accountId), ], );