From 0fa7971cb9fd4ac80027e86814b98c7c1b510248 Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Thu, 1 Jan 2026 16:43:55 +0000 Subject: [PATCH] Update --- apps/mobile/android/app/build.gradle | 1 + apps/mobile/android/build.gradle | 3 +- apps/mobile/app.json | 1 + apps/mobile/app/(app)/_layout.tsx | 244 ++++++++++++++++++++++++--- apps/mobile/google-services.json | 39 +++++ 5 files changed, 266 insertions(+), 22 deletions(-) create mode 100644 apps/mobile/google-services.json diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index a526c3d..a25b7fa 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" +apply plugin: "com.google.gms.google-services" def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle index dbda927..c1458f6 100644 --- a/apps/mobile/android/build.gradle +++ b/apps/mobile/android/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0' + buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0' minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24') compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35') targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35') @@ -18,6 +18,7 @@ buildscript { classpath('com.android.tools.build:gradle') classpath('com.facebook.react:react-native-gradle-plugin') classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') + classpath('com.google.gms:google-services:4.4.2') } } diff --git a/apps/mobile/app.json b/apps/mobile/app.json index e0bf46d..27146df 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -30,6 +30,7 @@ "backgroundColor": "#1a1a2e" }, "package": "com.sauci.app", + "googleServicesFile": "./google-services.json", "permissions": [ "RECEIVE_BOOT_COMPLETED", "VIBRATE", diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx index 183f85e..226ec61 100755 --- a/apps/mobile/app/(app)/_layout.tsx +++ b/apps/mobile/app/(app)/_layout.tsx @@ -35,35 +35,65 @@ interface PlayTabButtonProps { children?: React.ReactNode; onPress?: (e: any) => void; accessibilityState?: { selected?: boolean }; + isMenuOpen?: boolean; + onToggleMenu?: () => void; } -function PlayTabButton({ onPress, accessibilityState }: PlayTabButtonProps) { +interface RadialMenuItem { + id: string; + icon: keyof typeof Ionicons.glyphMap; + label: string; + angle: number; // degrees from top (0 = top, -45 = left, 45 = right) + color?: string; +} + +const RADIAL_MENU_ITEMS: RadialMenuItem[] = [ + { id: 'dares', icon: 'flash', label: 'Dares', angle: -50, color: '#FF6B35' }, + { id: 'match', icon: 'flame', label: 'Match', angle: 0 }, + { id: 'quiz', icon: 'help-circle', label: 'Quiz', angle: 50, color: '#7B68EE' }, +]; + +const RADIAL_DISTANCE = 100; // Distance from center button + +// Store for sharing state between PlayTabButton and AppLayout +let globalMenuToggle: (() => void) | null = null; +let globalMenuOpen = false; + +function PlayTabButton({ accessibilityState }: PlayTabButtonProps) { const isSelected = accessibilityState?.selected; + const [, forceUpdate] = useState({}); + + // Subscribe to global state changes + useEffect(() => { + const interval = setInterval(() => { + forceUpdate({}); + }, 50); + return () => clearInterval(interval); + }, []); return ( - + {/* Outer glow effect */} - {/* Circle container */} - - - - - - + {/* Main circle button */} + globalMenuToggle?.()}> + + + + + + + ); } @@ -83,6 +113,60 @@ export default function AppLayout() { const hasCheckedInitialBiometric = useRef(false); const wentToBackgroundAt = useRef(null); + // Radial menu state + const [isRadialMenuOpen, setIsRadialMenuOpen] = useState(false); + const radialMenuAnim = useRef(new Animated.Value(0)).current; + + const toggleRadialMenu = useCallback(() => { + const toValue = isRadialMenuOpen ? 0 : 1; + setIsRadialMenuOpen(!isRadialMenuOpen); + globalMenuOpen = !isRadialMenuOpen; + + Animated.spring(radialMenuAnim, { + toValue, + useNativeDriver: true, + tension: 100, + friction: 8, + }).start(); + }, [isRadialMenuOpen, radialMenuAnim]); + + const closeRadialMenu = useCallback(() => { + setIsRadialMenuOpen(false); + globalMenuOpen = false; + Animated.timing(radialMenuAnim, { + toValue: 0, + duration: 150, + useNativeDriver: true, + }).start(); + }, [radialMenuAnim]); + + const handleRadialMenuItemPress = useCallback((itemId: string) => { + closeRadialMenu(); + + // Navigate based on item + switch (itemId) { + case 'match': + router.push('/(app)/swipe'); + break; + case 'dares': + // TODO: Navigate to dares screen + console.log('Dares pressed - not yet implemented'); + break; + case 'quiz': + // TODO: Navigate to quiz screen + console.log('Quiz pressed - not yet implemented'); + break; + } + }, [router, closeRadialMenu]); + + // Register global menu toggle for PlayTabButton + useEffect(() => { + globalMenuToggle = toggleRadialMenu; + return () => { + globalMenuToggle = null; + }; + }, [toggleRadialMenu]); + // Check if we're on screens that should hide the tab bar const segmentStrings = segments as string[]; const isOnOnboarding = segmentStrings.includes("onboarding"); @@ -527,6 +611,78 @@ export default function AppLayout() { /> + {/* Radial Menu Overlay */} + {isRadialMenuOpen && ( + + )} + + {/* Radial Menu Items - positioned above the center button */} + + {RADIAL_MENU_ITEMS.map((item) => { + // Convert angle to radians and calculate position + const angleRad = (item.angle - 90) * (Math.PI / 180); // -90 to make 0 = top + const translateX = Math.cos(angleRad) * RADIAL_DISTANCE; + const translateY = Math.sin(angleRad) * RADIAL_DISTANCE; + + const itemScale = radialMenuAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0.3, 1], + }); + + const itemTranslateX = radialMenuAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0, translateX], + }); + + const itemTranslateY = radialMenuAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0, translateY], + }); + + return ( + + handleRadialMenuItemPress(item.id)} + style={radialStyles.menuItemButton} + > + + + + {item.label} + + + ); + })} + + {/* Match Notification Overlay - Using Animated.View instead of Modal for Android compatibility */} {matchNotification && (