Apply your knowledge to build something amazing!
ShareSpot is your M3 capstone project that brings together everything you've learned about location services, augmented reality, and health tracking into a single, production-ready social discovery platform. Users discover interesting places through an interactive map, visualize how locations look using AR previews, participate in fitness challenges with friends, and unlock premium features through in-app subscriptions.
This capstone demonstrates your ability to architect complex mobile applications that integrate multiple advanced features while maintaining clean code structure and excellent user experience. ShareSpot is more than a school project-it's a portfolio piece that showcases professional-level mobile development skills including social features, monetization strategies, and real-world app deployment.
By building ShareSpot, you'll create an app worthy of the App Store and Google Play, complete with user authentication, real-time updates, payment processing, and analytics. This is the project you'll show to potential employers to demonstrate your mastery of modern mobile development.
By completing this project, you will:
src/
├── components/
│ ├── common/
│ │ ├── Button.js
│ │ ├── Card.js
│ │ ├── Avatar.js
│ │ └── EmptyState.js
│ ├── discover/
│ │ ├── SpotCard.js
│ │ ├── MapView.js
│ │ └── CategoryFilter.js
│ ├── ar/
│ │ ├── ARPreview.js
│ │ └── ARControls.js
│ ├── social/
│ │ ├── FriendsList.js
│ │ ├── ChallengeCard.js
│ │ └── CommentSection.js
│ └── premium/
│ ├── SubscriptionPlans.js
│ └── PaymentSheet.js
├── screens/
│ ├── auth/
│ │ ├── LoginScreen.js
│ │ ├── RegisterScreen.js
│ │ └── OnboardingScreen.js
│ ├── discover/
│ │ ├── DiscoverScreen.js
│ │ ├── SpotDetailsScreen.js
│ │ └── AddSpotScreen.js
│ ├── ar/
│ │ └── ARPreviewScreen.js
│ ├── challenges/
│ │ ├── ChallengesScreen.js
│ │ ├── ChallengeDetailsScreen.js
│ │ └── CreateChallengeScreen.js
│ ├── profile/
│ │ ├── ProfileScreen.js
│ │ ├── EditProfileScreen.js
│ │ └── SettingsScreen.js
│ └── premium/
│ └── SubscriptionScreen.js
├── services/
│ ├── authService.js
│ ├── firestoreService.js
│ ├── spotService.js
│ ├── challengeService.js
│ ├── notificationService.js
│ ├── paymentService.js
│ └── analyticsService.js
├── contexts/
│ ├── AuthContext.js
│ ├── UserContext.js
│ └── PremiumContext.js
├── hooks/
│ ├── useAuth.js
│ ├── useSpots.js
│ ├── useChallenges.js
│ └── usePremium.js
├── navigation/
│ ├── AppNavigator.js
│ ├── AuthNavigator.js
│ └── MainTabNavigator.js
├── utils/
│ ├── constants.js
│ ├── helpers.js
│ └── validators.js
└── config/
└── firebase.js
// User Model
{
uid: string,
email: string,
displayName: string,
photoURL: string,
isPremium: boolean,
subscriptionExpiry: timestamp,
stats: {
spotsDiscovered: number,
spotsShared: number,
challengesCompleted: number,
totalSteps: number,
},
friends: [userId],
createdAt: timestamp,
}
// Spot Model
{
id: string,
name: string,
description: string,
category: string,
location: {
latitude: number,
longitude: number,
address: string,
},
photos: [photoURL],
rating: number,
createdBy: userId,
likes: [userId],
comments: [commentId],
arModelUrl: string, // Optional AR preview
createdAt: timestamp,
}
// Challenge Model
{
id: string,
name: string,
description: string,
type: 'steps' | 'distance' | 'spots',
goal: number,
startDate: timestamp,
endDate: timestamp,
participants: [userId],
leaderboard: [{
userId: string,
progress: number,
rank: number,
}],
createdBy: userId,
isPremium: boolean, // Premium-only challenges
}
ShareSpot uses Firebase as the backend, providing real-time data synchronization across devices. User authentication is handled by Firebase Auth, while Firestore stores all app data (spots, challenges, user profiles). When users discover new spots, data is written to Firestore and immediately synced to friends' feeds. Fitness data from Health Connect is periodically synced to update challenge leaderboards. Push notifications via Firebase Cloud Messaging alert users to friend activity. Stripe handles subscription payments, with premium status stored in Firestore.
Goal: Set up Firebase, authentication, and core navigation
Initialize project with all dependencies and Firebase setup.
npx create-expo-app ShareSpot
cd ShareSpot
# Install core dependencies
npx expo install firebase @react-native-firebase/app @react-native-firebase/auth
npx expo install @react-native-firebase/firestore @react-native-firebase/messaging
npm install @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs
npx expo install react-native-gesture-handler react-native-reanimated
# Install feature-specific packages
npx expo install expo-location react-native-maps
npx expo install expo-gl expo-three three
npx expo install react-native-health-connect
npm install victory-native react-native-svg
# Install payment and utilities
npm install @stripe/stripe-react-native
npx expo install expo-notifications expo-image-picker
npx expo install @react-native-async-storage/async-storage
Configure Firebase in src/config/firebase.js:
// src/config/firebase.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';
import { getAnalytics } from 'firebase/analytics';
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "your-app.firebaseapp.com",
projectId: "your-project-id",
storageBucket: "your-app.appspot.com",
messagingSenderId: "123456789",
appId: "1:123456789:web:abcdef",
measurementId: "G-XXXXXXXXXX"
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
export const analytics = getAnalytics(app);
export default app;
Create authentication system with email and social login.
// src/services/authService.js
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
updateProfile,
GoogleAuthProvider,
signInWithCredential,
} from 'firebase/auth';
import { doc, setDoc, getDoc } from 'firebase/firestore';
import { auth, db } from '../config/firebase';
import * as Google from 'expo-auth-session/providers/google';
export const registerWithEmail = async (email, password, displayName) => {
try {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
const user = userCredential.user;
// Update profile
await updateProfile(user, { displayName });
// Create user document in Firestore
await setDoc(doc(db, 'users', user.uid), {
uid: user.uid,
email: user.email,
displayName: displayName,
photoURL: null,
isPremium: false,
subscriptionExpiry: null,
stats: {
spotsDiscovered: 0,
spotsShared: 0,
challengesCompleted: 0,
totalSteps: 0,
},
friends: [],
createdAt: new Date(),
});
return user;
} catch (error) {
console.error('Registration error:', error);
throw error;
}
};
export const loginWithEmail = async (email, password) => {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
return userCredential.user;
} catch (error) {
console.error('Login error:', error);
throw error;
}
};
export const loginWithGoogle = async () => {
try {
const [request, response, promptAsync] = Google.useAuthRequest({
clientId: 'YOUR_GOOGLE_CLIENT_ID',
scopes: ['profile', 'email'],
});
if (response?.type === 'success') {
const { id_token } = response.params;
const credential = GoogleAuthProvider.credential(id_token);
const userCredential = await signInWithCredential(auth, credential);
// Check if user document exists, create if not
const userDoc = await getDoc(doc(db, 'users', userCredential.user.uid));
if (!userDoc.exists()) {
await setDoc(doc(db, 'users', userCredential.user.uid), {
uid: userCredential.user.uid,
email: userCredential.user.email,
displayName: userCredential.user.displayName,
photoURL: userCredential.user.photoURL,
isPremium: false,
stats: {
spotsDiscovered: 0,
spotsShared: 0,
challengesCompleted: 0,
totalSteps: 0,
},
friends: [],
createdAt: new Date(),
});
}
return userCredential.user;
}
} catch (error) {
console.error('Google login error:', error);
throw error;
}
};
export const logout = async () => {
try {
await signOut(auth);
} catch (error) {
console.error('Logout error:', error);
throw error;
}
};
Create auth context and login/register screens.
// src/contexts/AuthContext.js
import React, { createContext, useState, useEffect, useContext } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '../config/firebase';
const AuthContext = createContext();
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
return (
<AuthContext.Provider value={{ user, loading }}>
{children}
</AuthContext.Provider>
);
};
// src/screens/auth/LoginScreen.js
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { loginWithEmail, loginWithGoogle } from '../../services/authService';
export default function LoginScreen({ navigation }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const handleLogin = async () => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
setLoading(true);
try {
await loginWithEmail(email, password);
// Navigation handled by auth state change
} catch (error) {
Alert.alert('Login Failed', error.message);
} finally {
setLoading(false);
}
};
const handleGoogleLogin = async () => {
setLoading(true);
try {
await loginWithGoogle();
} catch (error) {
Alert.alert('Google Login Failed', error.message);
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome to ShareSpot</Text>
<Text style={styles.subtitle}>Discover, Share, Challenge</Text>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity
style={styles.button}
onPress={handleLogin}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Logging in...' : 'Log In'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.googleButton}
onPress={handleGoogleLogin}
disabled={loading}
>
<Text style={styles.googleButtonText}>Continue with Google</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
<Text style={styles.link}>Don't have an account? Sign up</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 32,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 10,
},
subtitle: {
fontSize: 18,
color: '#666',
textAlign: 'center',
marginBottom: 40,
},
input: {
backgroundColor: 'white',
padding: 15,
borderRadius: 8,
marginBottom: 15,
fontSize: 16,
},
button: {
backgroundColor: '#4CAF50',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginBottom: 10,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
googleButton: {
backgroundColor: 'white',
padding: 15,
borderRadius: 8,
alignItems: 'center',
borderWidth: 1,
borderColor: '#ddd',
marginBottom: 20,
},
googleButtonText: {
color: '#333',
fontSize: 16,
fontWeight: '600',
},
link: {
color: '#4CAF50',
textAlign: 'center',
fontSize: 16,
},
});
Goal: Implement discover, AR preview, and challenges features
Create spot management and discovery interface.
// src/services/spotService.js
import {
collection,
addDoc,
getDocs,
query,
where,
orderBy,
updateDoc,
doc,
arrayUnion,
arrayRemove,
increment,
} from 'firebase/firestore';
import { db } from '../config/firebase';
import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage';
export const createSpot = async (spotData, userId) => {
try {
const spot = {
...spotData,
createdBy: userId,
likes: [],
comments: [],
rating: 0,
createdAt: new Date(),
};
const docRef = await addDoc(collection(db, 'spots'), spot);
return { id: docRef.id, ...spot };
} catch (error) {
console.error('Create spot error:', error);
throw error;
}
};
export const getSpots = async (filters = {}) => {
try {
let q = collection(db, 'spots');
if (filters.category) {
q = query(q, where('category', '==', filters.category));
}
q = query(q, orderBy('createdAt', 'desc'));
const querySnapshot = await getDocs(q);
return querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
} catch (error) {
console.error('Get spots error:', error);
return [];
}
};
export const getNearbySpots = async (latitude, longitude, radiusKm = 10) => {
// Simple distance calculation (for production, use geohashing)
const spots = await getSpots();
return spots.filter(spot => {
const distance = calculateDistance(
latitude,
longitude,
spot.location.latitude,
spot.location.longitude
);
return distance <= radiusKm;
});
};
export const likeSpot = async (spotId, userId) => {
try {
const spotRef = doc(db, 'spots', spotId);
await updateDoc(spotRef, {
likes: arrayUnion(userId),
});
} catch (error) {
console.error('Like spot error:', error);
throw error;
}
};
export const unlikeSpot = async (spotId, userId) => {
try {
const spotRef = doc(db, 'spots', spotId);
await updateDoc(spotRef, {
likes: arrayRemove(userId),
});
} catch (error) {
console.error('Unlike spot error:', error);
throw error;
}
};
export const uploadSpotPhoto = async (uri, spotId) => {
try {
const storage = getStorage();
const filename = `spots/${spotId}/${Date.now()}.jpg`;
const storageRef = ref(storage, filename);
const response = await fetch(uri);
const blob = await response.blob();
await uploadBytes(storageRef, blob);
const downloadURL = await getDownloadURL(storageRef);
return downloadURL;
} catch (error) {
console.error('Upload photo error:', error);
throw error;
}
};
const calculateDistance = (lat1, lon1, lat2, lon2) => {
const R = 6371; // Earth's radius in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
};
// src/screens/discover/DiscoverScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import MapView, { Marker } from 'react-native-maps';
import { getSpots, getNearbySpots } from '../../services/spotService';
import { useAuth } from '../../contexts/AuthContext';
import SpotCard from '../../components/discover/SpotCard';
export default function DiscoverScreen({ navigation }) {
const { user } = useAuth();
const [spots, setSpots] = useState([]);
const [viewMode, setViewMode] = useState('list'); // 'list' or 'map'
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSpots();
}, []);
const loadSpots = async () => {
try {
const data = await getSpots();
setSpots(data);
} catch (error) {
console.error('Load spots error:', error);
} finally {
setLoading(false);
}
};
const renderSpot = ({ item }) => (
<SpotCard
spot={item}
onPress={() => navigation.navigate('SpotDetails', { spotId: item.id })}
/>
);
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Discover</Text>
<View style={styles.viewToggle}>
<TouchableOpacity
style={[styles.toggleButton, viewMode === 'list' && styles.activeToggle]}
onPress={() => setViewMode('list')}
>
<Text>📋</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.toggleButton, viewMode === 'map' && styles.activeToggle]}
onPress={() => setViewMode('map')}
>
<Text>🗺️</Text>
</TouchableOpacity>
</View>
</View>
{viewMode === 'list' ? (
<FlatList
data={spots}
renderItem={renderSpot}
keyExtractor={item => item.id}
contentContainerStyle={styles.list}
/>
) : (
<MapView
style={styles.map}
initialRegion={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
>
{spots.map(spot => (
<Marker
key={spot.id}
coordinate={{
latitude: spot.location.latitude,
longitude: spot.location.longitude,
}}
onPress={() => navigation.navigate('SpotDetails', { spotId: spot.id })}
/>
))}
</MapView>
)}
<TouchableOpacity
style={styles.fab}
onPress={() => navigation.navigate('AddSpot')}
>
<Text style={styles.fabText}>+</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
backgroundColor: 'white',
},
title: {
fontSize: 28,
fontWeight: 'bold',
},
viewToggle: {
flexDirection: 'row',
},
toggleButton: {
padding: 10,
marginLeft: 5,
},
activeToggle: {
backgroundColor: '#4CAF50',
borderRadius: 8,
},
list: {
padding: 10,
},
map: {
flex: 1,
},
fab: {
position: 'absolute',
bottom: 30,
right: 30,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#4CAF50',
justifyContent: 'center',
alignItems: 'center',
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
fabText: {
fontSize: 32,
color: 'white',
},
});
Implement fitness challenges with leaderboards.
// src/services/challengeService.js
import {
collection,
addDoc,
getDocs,
query,
where,
updateDoc,
doc,
arrayUnion,
} from 'firebase/firestore';
import { db } from '../config/firebase';
import { getStepsToday } from './healthService';
export const createChallenge = async (challengeData, userId) => {
try {
const challenge = {
...challengeData,
createdBy: userId,
participants: [userId],
leaderboard: [{
userId,
progress: 0,
rank: 1,
}],
createdAt: new Date(),
};
const docRef = await addDoc(collection(db, 'challenges'), challenge);
return { id: docRef.id, ...challenge };
} catch (error) {
console.error('Create challenge error:', error);
throw error;
}
};
export const getChallenges = async (userId) => {
try {
const q = query(
collection(db, 'challenges'),
where('participants', 'array-contains', userId)
);
const querySnapshot = await getDocs(q);
return querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
} catch (error) {
console.error('Get challenges error:', error);
return [];
}
};
export const joinChallenge = async (challengeId, userId) => {
try {
const challengeRef = doc(db, 'challenges', challengeId);
await updateDoc(challengeRef, {
participants: arrayUnion(userId),
leaderboard: arrayUnion({
userId,
progress: 0,
rank: 0,
}),
});
} catch (error) {
console.error('Join challenge error:', error);
throw error;
}
};
export const updateChallengeProgress = async (challengeId, userId) => {
try {
// Get current steps from Health Connect
const steps = await getStepsToday();
// Update leaderboard (simplified - in production, use cloud function)
const challengeRef = doc(db, 'challenges', challengeId);
// This would need more complex logic to update leaderboard array
// Consider using Cloud Functions for atomic updates
} catch (error) {
console.error('Update progress error:', error);
throw error;
}
};
Goal: Implement monetization and advanced features
Create Stripe integration for premium subscriptions.
// src/services/paymentService.js
import { StripeProvider, useStripe } from '@stripe/stripe-react-native';
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '../config/firebase';
const STRIPE_PUBLISHABLE_KEY = 'YOUR_STRIPE_PUBLISHABLE_KEY';
export const initializePayment = async () => {
// Initialize Stripe
};
export const subscribeToPremium = async (userId, planId) => {
try {
// In production, call your backend to create subscription
// Backend would use Stripe API to process payment
// For now, simulate premium activation
const userRef = doc(db, 'users', userId);
await updateDoc(userRef, {
isPremium: true,
subscriptionExpiry: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
});
return true;
} catch (error) {
console.error('Subscription error:', error);
throw error;
}
};
export const cancelSubscription = async (userId) => {
try {
const userRef = doc(db, 'users', userId);
await updateDoc(userRef, {
isPremium: false,
subscriptionExpiry: null,
});
} catch (error) {
console.error('Cancel subscription error:', error);
throw error;
}
};
Goal: Final testing, optimization, and app store deployment
Configure app.json for production deployment.
{
"expo": {
"name": "ShareSpot",
"slug": "sharespot",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#4CAF50"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.sharespot",
"buildNumber": "1.0.0"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#4CAF50"
},
"package": "com.yourcompany.sharespot",
"versionCode": 1
},
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
# Install EAS CLI
npm install -g eas-cli
# Login to Expo
eas login
# Configure build
eas build:configure
# Build for iOS
eas build --platform ios
# Build for Android
eas build --platform android
# Submit to App Store
eas submit --platform ios
# Submit to Google Play
eas submit --platform android
💡 Tip: Test premium features thoroughly before deployment. Use Stripe test mode during development and switch to live keys only for production builds.
| Criteria | Points | Description |
|---|---|---|
| Functionality | 40 | All features work: auth, discovery, AR, challenges, social, premium |
| Code Quality | 25 | Clean architecture, efficient Firebase usage, proper error handling |
| Design/UX | 20 | Professional UI, smooth navigation, engaging onboarding |
| Documentation | 10 | Comprehensive README, deployment guide, API documentation |
| Creativity | 5 | Unique features, polish, attention to detail |
| Total | 100 |
| Week | Milestone | Deliverable |
|---|---|---|
| 1 | Phase 1-2 Complete | Auth working, discovery with spots, basic challenges |
| 2 | Phase 3-4 Complete | Premium features, deployed to App Store and Google Play |
For students who finish early:
After completing this capstone:
Congratulations on completing M3! ShareSpot demonstrates your mastery of advanced mobile development and positions you for success in M4's AI integration.