diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index 6623142a..4167b377 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -17,4 +17,7 @@ web-build/ # The following patterns were generated by expo-cli expo-env.d.ts -# @end expo-cli \ No newline at end of file +# @end expo-cli + +# build artifacts +*.tar.gz \ No newline at end of file diff --git a/apps/mobile/app/(app)/(tabs)/_layout.tsx b/apps/mobile/app/(app)/(tabs)/_layout.tsx index 2f47aca4..a7491eeb 100644 --- a/apps/mobile/app/(app)/(tabs)/_layout.tsx +++ b/apps/mobile/app/(app)/(tabs)/_layout.tsx @@ -1,23 +1,16 @@ +import { TabBar } from '@/components/common/tab-bar' import { Button } from '@/components/ui/button' import { Text } from '@/components/ui/text' import { useColorScheme } from '@/hooks/useColorScheme' import { theme } from '@/lib/theme' -import { cn } from '@/lib/utils' import { useUser } from '@clerk/clerk-expo' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' -import * as Haptics from 'expo-haptics' import { Link, Tabs } from 'expo-router' -import { - // BarChartBigIcon, - CogIcon, - LandPlotIcon, - PlusIcon, - WalletIcon, -} from 'lucide-react-native' +import { PlusIcon } from 'lucide-react-native' import { usePostHog } from 'posthog-react-native' import { useEffect } from 'react' -import { View, useWindowDimensions } from 'react-native' +import { useWindowDimensions } from 'react-native' import Purchases from 'react-native-purchases' import { useSafeAreaInsets } from 'react-native-safe-area-context' @@ -43,6 +36,7 @@ export default function TabLayout() { return ( } screenOptions={{ headerShadowVisible: false, tabBarActiveTintColor: theme[colorScheme ?? 'light'].background, @@ -75,17 +69,6 @@ export default function TabLayout() { name="index" options={{ headerShown: false, - tabBarShowLabel: false, - tabBarIcon: ({ color, focused }) => ( - - - - ), }} /> ( - - - - ), headerRight: () => ( - - - ), }} /> diff --git a/apps/mobile/app/(app)/(tabs)/budgets.tsx b/apps/mobile/app/(app)/(tabs)/budgets.tsx index f4af6858..94bcf03b 100644 --- a/apps/mobile/app/(app)/(tabs)/budgets.tsx +++ b/apps/mobile/app/(app)/(tabs)/budgets.tsx @@ -150,7 +150,10 @@ export default function BudgetsScreen() { }, 0) const chartData = map( - groupBy(transactions, (t) => t.date), + groupBy( + transactions.filter((i) => !!i.budgetId), + (t) => t.date, + ), (transactions, key) => ({ day: new Date(key).getDate(), amount: transactions.reduce((acc, t) => acc - t.amountInVnd, 0), diff --git a/apps/mobile/app/(app)/(tabs)/dummy.tsx b/apps/mobile/app/(app)/(tabs)/dummy.tsx deleted file mode 100644 index 01730b3b..00000000 --- a/apps/mobile/app/(app)/(tabs)/dummy.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// Dump screen for create transaction tab button -export default function DummyScreen() { - return null -} diff --git a/apps/mobile/app/(app)/(tabs)/index.tsx b/apps/mobile/app/(app)/(tabs)/index.tsx index d96f7c1c..c1b86855 100644 --- a/apps/mobile/app/(app)/(tabs)/index.tsx +++ b/apps/mobile/app/(app)/(tabs)/index.tsx @@ -13,6 +13,7 @@ import { useColorScheme } from '@/hooks/useColorScheme' import { formatDateShort } from '@/lib/date' import { theme } from '@/lib/theme' import { useTransactionList } from '@/stores/transaction/hooks' +import { useTransactionStore } from '@/stores/transaction/store' import { dayjsExtended } from '@6pm/utilities' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' @@ -54,6 +55,7 @@ export default function HomeScreen() { categoryId, ...timeRange, }) + const { draftTransactions } = useTransactionStore() const handleSetFilter = (filter: HomeFilter) => { if (filter === HomeFilter.ByDay) { @@ -165,7 +167,7 @@ export default function HomeScreen() { } extraData={filter} /> - {!transactions.length && !isLoading && ( + {!transactions.length && !draftTransactions.length && !isLoading && ( <> {filter === HomeFilter.All ? ( diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx index c63e1d65..04ccb783 100644 --- a/apps/mobile/app/(app)/_layout.tsx +++ b/apps/mobile/app/(app)/_layout.tsx @@ -69,6 +69,9 @@ export default function AuthenticatedLayout() { options={{ presentation: 'modal', headerTitle: '', + headerStyle: { + backgroundColor: theme[colorScheme ?? 'light'].muted, + }, // headerShown: false, }} /> diff --git a/apps/mobile/app/(app)/paywall.tsx b/apps/mobile/app/(app)/paywall.tsx index 9d776605..f6dd3450 100644 --- a/apps/mobile/app/(app)/paywall.tsx +++ b/apps/mobile/app/(app)/paywall.tsx @@ -1,4 +1,3 @@ -import { AmountFormat } from '@/components/common/amount-format' import { Marquee } from '@/components/common/marquee' import { toast } from '@/components/common/toast' import { PaywallIllustration } from '@/components/svg-assets/paywall-illustration' @@ -104,13 +103,11 @@ function PackageCard({ {t(i18n)`months`} - + + {isAnnual + ? data.product.pricePerYearString + : data.product.pricePerMonthString} + ) diff --git a/apps/mobile/app/(app)/transaction/new-record.tsx b/apps/mobile/app/(app)/transaction/new-record.tsx index 9829f924..843f1c05 100644 --- a/apps/mobile/app/(app)/transaction/new-record.tsx +++ b/apps/mobile/app/(app)/transaction/new-record.tsx @@ -82,14 +82,14 @@ export default function NewRecordScreen() { animated: true, }) }} - className="w-[150px]" + className="w-[160px]" > - + - + - + diff --git a/apps/mobile/components/auth/auth-local.tsx b/apps/mobile/components/auth/auth-local.tsx index 1367a0b0..7f45a6a8 100644 --- a/apps/mobile/components/auth/auth-local.tsx +++ b/apps/mobile/components/auth/auth-local.tsx @@ -29,7 +29,7 @@ export function AuthLocal({ onAuthenticated }: AuthLocalProps) { }, []) return ( - + {t( i18n, diff --git a/apps/mobile/components/common/amount-format.tsx b/apps/mobile/components/common/amount-format.tsx index 4d32239a..1139f4bf 100644 --- a/apps/mobile/components/common/amount-format.tsx +++ b/apps/mobile/components/common/amount-format.tsx @@ -10,7 +10,7 @@ const SHOULD_ROUND_VALUE_CURRENCIES = ['VND'] const amountVariants = cva('line-clamp-1 shrink-0 font-semibold', { variants: { size: { - xl: 'text-4xl', + xl: 'text-5xl', lg: 'text-3xl', md: 'text-2xl', sm: 'text-base', diff --git a/apps/mobile/components/common/tab-bar.tsx b/apps/mobile/components/common/tab-bar.tsx new file mode 100644 index 00000000..3e37395f --- /dev/null +++ b/apps/mobile/components/common/tab-bar.tsx @@ -0,0 +1,133 @@ +import { cn } from '@/lib/utils' +import type { BottomTabBarProps } from '@react-navigation/bottom-tabs' +import * as Haptics from 'expo-haptics' +import { Link } from 'expo-router' +import { + CogIcon, + LandPlotIcon, + type LucideIcon, + PlusIcon, + WalletIcon, +} from 'lucide-react-native' +import { rem } from 'nativewind' +import { Pressable, type PressableProps, View } from 'react-native' +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import { Button } from '../ui/button' +import { Separator } from '../ui/separator' + +type TabBarItemProps = { + focused: boolean + icon: LucideIcon + descriptor: BottomTabBarProps['descriptors'][string] +} +function TabBarItem({ + icon: Icon, + focused, + descriptor, + ...props +}: TabBarItemProps & PressableProps) { + const { options } = descriptor + return ( + + + + ) +} + +function NewRecordButton() { + return ( + + + + ) +} + +const TAB_BAR_ICONS = { + index: WalletIcon, + budgets: LandPlotIcon, + settings: CogIcon, +} + +const TAB_BAR_ITEM_WIDTH = (3 + 0.75) * rem.get() + +export function TabBar({ state, descriptors, navigation }: BottomTabBarProps) { + const tabIndicatorPosition = useSharedValue(0) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: tabIndicatorPosition.value }], + })) + + return ( + + + {state.routes.map((route, index) => { + function onPress() { + Haptics.selectionAsync() + + tabIndicatorPosition.value = withTiming(index * TAB_BAR_ITEM_WIDTH, { + duration: 300, + }) + + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true, + }) + + if (state.index !== index && !event.defaultPrevented) { + navigation.navigate(route.name, route.params) + } + } + + function onLongPress() { + navigation.emit({ + type: 'tabLongPress', + target: route.key, + }) + } + + return ( + + ) + })} + + + + ) +} diff --git a/apps/mobile/components/home/wallet-statistics.tsx b/apps/mobile/components/home/wallet-statistics.tsx index a34892d5..03373629 100644 --- a/apps/mobile/components/home/wallet-statistics.tsx +++ b/apps/mobile/components/home/wallet-statistics.tsx @@ -128,7 +128,7 @@ export function WalletStatistics({ className="!border-0 h-auto native:h-auto flex-col items-center gap-3" > - + {options.find((option) => option.value === view)?.label} diff --git a/apps/mobile/components/primitives/tabs/tabs.tsx b/apps/mobile/components/primitives/tabs/tabs.tsx index 41a23162..8bfb3240 100644 --- a/apps/mobile/components/primitives/tabs/tabs.tsx +++ b/apps/mobile/components/primitives/tabs/tabs.tsx @@ -15,18 +15,7 @@ interface RootContext extends TabsRootProps { const TabsContext = React.createContext(null) const Root = React.forwardRef( - ( - { - asChild, - value, - onValueChange, - orientation: Orientation, - dir: Dir, - activationMode: ActivationMode, - ...viewProps - }, - ref, - ) => { + ({ asChild, value, onValueChange, ...viewProps }, ref) => { const nativeID = React.useId() const Component = asChild ? Slot.View : View return ( diff --git a/apps/mobile/components/ui/tabs.tsx b/apps/mobile/components/ui/tabs.tsx index e69a4f37..4b605108 100644 --- a/apps/mobile/components/ui/tabs.tsx +++ b/apps/mobile/components/ui/tabs.tsx @@ -12,7 +12,7 @@ const TabsList = React.forwardRef<