mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Adds a shareable https://sauci.app/join/{code} link as an alternative to manually typing an 8-character invite code: - apps/web: /join/[code] page that validates the code shape client-side, attempts the app.sauci:// scheme link, and falls back to a large code display with copy button plus App Store/Play Store links; PostHog events for page view, code copy, and store-button clicks. - apps/web: apple-app-site-association and assetlinks.json under /.well-known so iOS Universal Links and Android App Links can verify sauci.app for /join/*. The Android release cert SHA-256 fingerprint is a placeholder pending the real value. - apps/mobile: associated domains (iOS) and an intent filter (Android) for https://sauci.app/join/*, alongside the existing app.sauci scheme. - apps/mobile: parse incoming join links, pre-fill and highlight the code on the pairing screen, stash the code for signed-out/mid-onboarding users and apply it once they reach pairing, and offer a once-per-session clipboard hand-off that is never auto-applied. - apps/mobile: share message now leads with the join link, keeping the raw code visible as a fallback. - Analytics: invite_link_opened, pairing_code_prefilled (mobile); join_page_viewed, join_page_code_copied, join_page_store_button_clicked (web). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
84 lines
2.7 KiB
TypeScript
Executable File
84 lines
2.7 KiB
TypeScript
Executable File
import { useEffect, useState } from "react";
|
|
import { Redirect } from "expo-router";
|
|
import { useAuthStore } from "../src/store";
|
|
import { View, ActivityIndicator, StyleSheet } from "react-native";
|
|
import { needsOnboarding } from "../src/constants/onboarding";
|
|
import { getPendingInviteCode } from "../src/lib/pendingInviteCode";
|
|
|
|
export default function Index() {
|
|
const { isAuthenticated, isLoading, user } = useAuthStore();
|
|
const [pendingCodeChecked, setPendingCodeChecked] = useState(false);
|
|
const [hasPendingInvite, setHasPendingInvite] = useState(false);
|
|
|
|
// Check once, after auth resolves, whether an invite code is waiting to be
|
|
// applied (e.g. the user tapped a join link before signing in).
|
|
useEffect(() => {
|
|
if (isLoading || !isAuthenticated || user?.couple_id) {
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
getPendingInviteCode().then((code) => {
|
|
if (cancelled) return;
|
|
setHasPendingInvite(!!code);
|
|
setPendingCodeChecked(true);
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [isLoading, isAuthenticated, user?.couple_id]);
|
|
|
|
// Show loading state while auth is being determined
|
|
if (isLoading) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<ActivityIndicator size="large" color="#e94560" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// Redirect based on auth state
|
|
if (!isAuthenticated) {
|
|
return <Redirect href="/(auth)/login" />;
|
|
}
|
|
|
|
// If user hasn't completed onboarding or needs to re-onboard, go to onboarding
|
|
if (needsOnboarding(user?.onboarding_completed, user?.onboarding_version)) {
|
|
return <Redirect href="/(app)/onboarding" />;
|
|
}
|
|
|
|
// A couple already exists for this user; no invite hand-off is relevant.
|
|
if (user?.couple_id) {
|
|
return <Redirect href="/(app)" />;
|
|
}
|
|
|
|
// Wait for the one-time pending invite code check before deciding the
|
|
// route, so a stashed code from a join link is applied without an extra
|
|
// trip through the home screen.
|
|
if (!pendingCodeChecked) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<ActivityIndicator size="large" color="#e94560" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (hasPendingInvite) {
|
|
return <Redirect href="/(app)/pairing" />;
|
|
}
|
|
|
|
// Go to home/dashboard - users can pair from within the app
|
|
// No forced redirect to pairing for users without a couple
|
|
return <Redirect href="/(app)" />;
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
backgroundColor: "#1a1a2e",
|
|
},
|
|
});
|