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<