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 && (