Expose mobile More tools

This commit is contained in:
2026-06-29 01:38:00 -04:00
parent 8ad4210908
commit 7bc8559b44
3 changed files with 168 additions and 0 deletions
+10
View File
@@ -3,6 +3,7 @@ import { NativeTabs } from "expo-router/unstable-native-tabs";
import { AppLockOverlay } from "@/components/AppLockOverlay";
import { InvoiceReminderSync } from "@/components/InvoiceReminderSync";
import { OnboardingGate } from "@/components/OnboardingGate";
import { ShortcutHandler } from "@/components/ShortcutHandler";
import { TimeClockLiveActivitySync } from "@/components/time-clock/TimeClockLiveActivitySync";
import { useAppTheme } from "@/contexts/ThemeContext";
@@ -73,7 +74,16 @@ export default function AppLayout() {
/>
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="more" contentStyle={tabContentStyle} disableAutomaticContentInsets>
<NativeTabs.Trigger.Icon
sf={{ default: "ellipsis.circle", selected: "ellipsis.circle.fill" }}
md="more_horiz"
/>
<NativeTabs.Trigger.Label>More</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
</NativeTabs>
<OnboardingGate />
<InvoiceReminderSync />
<TimeClockLiveActivitySync />
<ShortcutHandler />
+139
View File
@@ -0,0 +1,139 @@
import { router } from "expo-router";
import { useState } from "react";
import { ScrollView, StyleSheet, Text, View } from "react-native";
import { AppBackground } from "@/components/AppBackground";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { api } from "@/lib/trpc";
export default function OnboardingScreen() {
const { colors } = useAppTheme();
const utils = api.useUtils();
const statusQuery = api.settings.getOnboardingStatus.useQuery();
const [businessName, setBusinessName] = useState("");
const [clientName, setClientName] = useState("");
const [step, setStep] = useState(0);
const createBusiness = api.businesses.create.useMutation();
const createClient = api.clients.create.useMutation();
const complete = api.settings.completeOnboarding.useMutation({
onSuccess: async () => {
await utils.settings.getOnboardingStatus.invalidate();
await utils.settings.getProfile.invalidate();
router.replace("/(app)");
},
});
async function finish() {
if (businessName.trim()) {
await createBusiness.mutateAsync({
name: businessName.trim(),
isDefault: true,
});
}
if (clientName.trim()) {
await createClient.mutateAsync({
name: clientName.trim(),
currency: "USD",
});
}
await complete.mutateAsync();
}
const steps = [
{
title: "Welcome to beenvoice",
body: "Set up your workspace in a minute — business profile and first client.",
},
{
title: "Your business",
body: "This appears on invoices you send to clients.",
},
{
title: "First client",
body: "Add someone you bill. You can skip and add clients later.",
},
];
const current = steps[step]!;
return (
<AppBackground>
<ScrollView contentContainerStyle={styles.body}>
<Text style={[styles.kicker, { color: colors.mutedForeground }]}>
Step {step + 1} of {steps.length}
</Text>
<Text style={[styles.title, { color: colors.foreground }]}>{current.title}</Text>
<Text style={[styles.bodyText, { color: colors.mutedForeground }]}>{current.body}</Text>
{step === 1 ? (
<Input
label="Business name"
value={businessName}
onChangeText={setBusinessName}
placeholder="Your studio or company"
/>
) : null}
{step === 2 ? (
<Input
label="Client name"
value={clientName}
onChangeText={setClientName}
placeholder="Acme Corp"
/>
) : null}
<View style={styles.actions}>
{step > 0 ? (
<Button title="Back" variant="secondary" onPress={() => setStep((s) => s - 1)} />
) : null}
{step < steps.length - 1 ? (
<Button title="Continue" onPress={() => setStep((s) => s + 1)} />
) : (
<Button
title={complete.isPending ? "Finishing…" : "Go to dashboard"}
loading={complete.isPending}
onPress={() => void finish()}
/>
)}
</View>
{statusQuery.data && !statusQuery.data.completed ? (
<Button title="Skip for now" variant="secondary" onPress={() => void complete.mutateAsync()} />
) : null}
</ScrollView>
</AppBackground>
);
}
const styles = StyleSheet.create({
body: {
padding: spacing.lg,
gap: spacing.md,
minHeight: "100%",
justifyContent: "center",
},
kicker: {
fontFamily: fonts.bodyMedium,
fontSize: 13,
},
title: {
fontFamily: fonts.heading,
fontSize: 32,
lineHeight: 36,
},
bodyText: {
fontFamily: fonts.body,
fontSize: 16,
lineHeight: 24,
},
actions: {
gap: spacing.sm,
marginTop: spacing.lg,
},
});
+19
View File
@@ -0,0 +1,19 @@
import { router } from "expo-router";
import { useEffect, useRef } from "react";
import { api } from "@/lib/trpc";
/** Redirect new users into onboarding until they complete setup. */
export function OnboardingGate() {
const statusQuery = api.settings.getOnboardingStatus.useQuery();
const redirected = useRef(false);
useEffect(() => {
if (redirected.current || statusQuery.isLoading || !statusQuery.data) return;
if (statusQuery.data.completed) return;
redirected.current = true;
router.push("/(app)/onboarding" as never);
}, [statusQuery.data, statusQuery.isLoading]);
return null;
}