Fix onboarding gate + tab icons + trim New-card screen
CI / policy (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / profile (push) Has been cancelled
CI / mobile (push) Has been cancelled

- Onboarding gate: the `hydrated` flag never flipped (onRehydrateStorage hit the
  store const during synchronous MMKV hydration), leaving a blank null gate.
  Dropped the flag; gate directly on `onboarded` (MMKV hydrates synchronously).
- Tab bar: removed the item padding that clipped the icons; clean taller bar
  with safe-area bottom padding.
- New-card screen: header title "Make your card" (centered), removed the body
  heading and all the detail fields + back-of-card links (captured in
  onboarding) and the Publish button (returns later on the flipped card). Keeps
  Take/Choose photo + AI scene/video + Save.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-05 23:46:12 -05:00
co-authored by Claude Opus 4.8
parent 8865dc9683
commit e77e812609
3 changed files with 9 additions and 31 deletions
+4 -7
View File
@@ -19,12 +19,10 @@ function tabIcon(name: IconName) {
export default function TabsLayout() { export default function TabsLayout() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const hydrated = useProfileStore((s) => s.hydrated);
const onboarded = useProfileStore((s) => s.onboarded); const onboarded = useProfileStore((s) => s.onboarded);
// Wait for MMKV to rehydrate before deciding, then send first-run users to // First-run users go to onboarding (MMKV hydrates synchronously, so this is
// onboarding (avoids a flash of the tabs). // correct on the first render — no flash).
if (!hydrated) return null;
if (!onboarded) return <Redirect href="/onboarding" />; if (!onboarded) return <Redirect href="/onboarding" />;
return ( return (
@@ -38,11 +36,10 @@ export default function TabsLayout() {
borderTopWidth: StyleSheet.hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#1c1c22", borderTopColor: "#1c1c22",
height: 64 + insets.bottom, height: 64 + insets.bottom,
paddingTop: 10, paddingTop: 8,
paddingBottom: Math.max(insets.bottom, 14), paddingBottom: insets.bottom + 10,
}, },
tabBarLabelStyle: { fontSize: 11, fontWeight: "600", marginBottom: 2 }, tabBarLabelStyle: { fontSize: 11, fontWeight: "600", marginBottom: 2 },
tabBarItemStyle: { paddingVertical: 4 },
}} }}
> >
<Tabs.Screen name="index" options={{ title: "Cards", tabBarIcon: tabIcon("bag-personal") }} /> <Tabs.Screen name="index" options={{ title: "Cards", tabBarIcon: tabIcon("bag-personal") }} />
+3 -19
View File
@@ -269,8 +269,9 @@ export default function CardEditorScreen() {
return ( return (
<ScrollView style={styles.root} contentContainerStyle={styles.content}> <ScrollView style={styles.root} contentContainerStyle={styles.content}>
<Stack.Screen options={{ title: cardId ? "Edit card" : "New card" }} /> <Stack.Screen
<Text style={styles.h1}>{cardId ? "Edit your card" : "Make your card"}</Text> options={{ title: cardId ? "Edit card" : "Make your card", headerTitleAlign: "center" }}
/>
{videoUri ? ( {videoUri ? (
<Video <Video
@@ -309,14 +310,6 @@ export default function CardEditorScreen() {
</Pressable> </Pressable>
</View> </View>
<Text style={styles.sectionLabel}>Details · prefilled from your profile</Text>
<Field label="Name" value={name} onChangeText={setName} />
<Field label="Title" value={title} onChangeText={setTitle} />
<Field label="QR link" value={url} onChangeText={setUrl} autoCapitalize="none" />
<Text style={styles.sectionLabel}>Back of card · scannable links</Text>
<LinksEditor links={links} onChange={setLinks} />
<Pressable <Pressable
style={[styles.cta, (!media || saving) && styles.ctaDisabled]} style={[styles.cta, (!media || saving) && styles.ctaDisabled]}
disabled={!media || saving} disabled={!media || saving}
@@ -325,15 +318,6 @@ export default function CardEditorScreen() {
{saving ? <ActivityIndicator color="#fff" /> : <Text style={styles.ctaText}>Save card</Text>} {saving ? <ActivityIndicator color="#fff" /> : <Text style={styles.ctaText}>Save card</Text>}
</Pressable> </Pressable>
<Pressable
style={[styles.publishBtn, saving && styles.ctaDisabled]}
disabled={saving}
onPress={goPublish}
>
<MaterialCommunityIcons name="web" size={20} color="#f5f5f7" />
<Text style={styles.photoText}>Publish to web · AI welcome</Text>
</Pressable>
{cardId && ( {cardId && (
<Pressable onPress={onDelete}> <Pressable onPress={onDelete}>
<Text style={styles.deleteText}>Delete card</Text> <Text style={styles.deleteText}>Delete card</Text>
+2 -5
View File
@@ -12,18 +12,18 @@ const storage = new MMKV({ id: "cardclaws-profile" });
interface ProfileState { interface ProfileState {
onboarded: boolean; onboarded: boolean;
profile: Profile; profile: Profile;
hydrated: boolean;
setProfile: (patch: Partial<Profile>) => void; setProfile: (patch: Partial<Profile>) => void;
completeOnboarding: (p: Profile) => void; completeOnboarding: (p: Profile) => void;
reset: () => void; reset: () => void;
} }
// MMKV via createJSONStorage hydrates synchronously, so `onboarded` is correct
// on the first render — no async hydration flag needed (matches localCardsStore).
export const useProfileStore = create<ProfileState>()( export const useProfileStore = create<ProfileState>()(
persist( persist(
(set) => ({ (set) => ({
onboarded: false, onboarded: false,
profile: EMPTY_PROFILE, profile: EMPTY_PROFILE,
hydrated: false,
setProfile: (patch) => set((s) => ({ profile: { ...s.profile, ...patch } })), setProfile: (patch) => set((s) => ({ profile: { ...s.profile, ...patch } })),
completeOnboarding: (p) => set({ profile: p, onboarded: true }), completeOnboarding: (p) => set({ profile: p, onboarded: true }),
reset: () => set({ profile: EMPTY_PROFILE, onboarded: false }), reset: () => set({ profile: EMPTY_PROFILE, onboarded: false }),
@@ -36,9 +36,6 @@ export const useProfileStore = create<ProfileState>()(
removeItem: (k) => storage.delete(k), removeItem: (k) => storage.delete(k),
})), })),
partialize: (s) => ({ onboarded: s.onboarded, profile: s.profile }), partialize: (s) => ({ onboarded: s.onboarded, profile: s.profile }),
onRehydrateStorage: () => () => {
useProfileStore.setState({ hydrated: true });
},
}, },
), ),
); );