diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..01bd409 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Copy this file to `.env` and fill in your values +# These are consumed by app.config.js via dotenv at build time + +# Firebase config (also present in google-services.json / GoogleService-Info.plist for native) +FIREBASE_API_KEY= +FIREBASE_AUTH_DOMAIN= +FIREBASE_PROJECT_ID= +FIREBASE_STORAGE_BUCKET= +FIREBASE_MESSAGING_SENDER_ID= +FIREBASE_APP_ID= + +# Google Sign-In: REQUIRED for offline access and proper sign-in on native +# Provide the OAuth 2.0 Client ID of type "Web application" from Google Cloud console (same project as your iOS/Android clients) +GOOGLE_WEB_CLIENT_ID= + +# Optional: Maps (Android) +GOOGLE_MAPS_API_KEY= + +# Build metadata +BUILD_DATE= diff --git a/.github/workflows/eas-android-build.yml b/.github/workflows/eas-android-build.yml index f52813d..84c851b 100644 --- a/.github/workflows/eas-android-build.yml +++ b/.github/workflows/eas-android-build.yml @@ -4,7 +4,7 @@ permissions: on: push: - branches: [main] + branches: ["**"] # Run on all branches workflow_dispatch: env: @@ -21,6 +21,7 @@ jobs: build_number: ${{ steps.version-control.outputs.build_number }} build_date: ${{ steps.version-control.outputs.build_date }} is_production: ${{ steps.version-control.outputs.is_production }} + branch_name: ${{ steps.extract-branch.outputs.branch_name }} steps: # ======================== # πŸ› οΈ Repository Setup @@ -30,6 +31,14 @@ jobs: with: fetch-depth: 0 + - name: "πŸ” Extract branch name" + id: extract-branch + shell: bash + run: | + BRANCH_NAME=${GITHUB_REF#refs/heads/} + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "Branch: $BRANCH_NAME" + # ======================== # βš™οΈ Environment Configuration # ======================== @@ -73,7 +82,12 @@ jobs: APP_VERSION=$(jq -r '.version' version.json) IS_PRODUCTION="true" else - APP_VERSION="1.0.0-prerelease.${{ github.run_number }}" + # For non-main branches, create a prerelease version with branch name + BRANCH_NAME=${{ steps.extract-branch.outputs.branch_name }} + SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9]/-/g') + # Get base version from version.json + BASE_VERSION=$(jq -r '.version' version.json) + APP_VERSION="${BASE_VERSION}-pre.${SANITIZED_BRANCH}.${{ github.run_number }}" IS_PRODUCTION="false" fi @@ -109,7 +123,17 @@ jobs: run: | echo "πŸ”„ Initializing build process..." sudo apt-get install -y jq - BUILD_JSON=$(npx eas build -p android --profile production --non-interactive --json) + + # Choose profile based on branch + if [ "${{ github.ref }}" == "refs/heads/main" ]; then + BUILD_PROFILE="production" + else + BUILD_PROFILE="preview" # Use a different profile for pre-releases if needed + fi + + echo "Using build profile: $BUILD_PROFILE" + + BUILD_JSON=$(npx eas build -p android --profile $BUILD_PROFILE --non-interactive --json) echo "Raw build output: $BUILD_JSON" BUILD_ID=$(echo "$BUILD_JSON" | jq -r '.[0].id') if [[ -z "$BUILD_ID" || "$BUILD_ID" == "null" ]]; then @@ -274,7 +298,6 @@ jobs: CHANGELOG=$(git log --pretty=format:"- %s (%h) by %an" -n 15) echo "$CHANGELOG" > changelog.txt - # FIXED OUTPUT HANDLING (ONLY CHANGE) delimiter=$(openssl rand -hex 6) echo "CHANGELOG<<${delimiter}" >> $GITHUB_OUTPUT cat changelog.txt >> $GITHUB_OUTPUT @@ -316,11 +339,14 @@ jobs: RELEASE_TAG="v${{ needs.build-android.outputs.app_version }}" RELEASE_TITLE="Production Release v${{ needs.build-android.outputs.app_version }}" else - echo "🟑 Nightly build detected" - RELEASE_TAG="nightly-${{ needs.build-android.outputs.build_date }}" - RELEASE_TITLE="Nightly Build (${{ needs.build-android.outputs.build_date }})" + echo "🟑 Pre-release build detected" + BRANCH_NAME="${{ needs.build-android.outputs.branch_name }}" + RELEASE_TAG="prerelease-${BRANCH_NAME}-${{ needs.build-android.outputs.build_date }}" + RELEASE_TITLE="Pre-release (${BRANCH_NAME}) v${{ needs.build-android.outputs.app_version }}" fi echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_OUTPUT + echo "RELEASE_TITLE=${RELEASE_TITLE}" >> $GITHUB_OUTPUT + - name: "πŸŽ‰ Publish GitHub Release" uses: softprops/action-gh-release@v2 with: diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist index b014b2e..7fb687e 100644 --- a/GoogleService-Info.plist +++ b/GoogleService-Info.plist @@ -24,6 +24,10 @@ IS_SIGNIN_ENABLED + CLIENT_ID + 906784991953-aoq1tgiac6ge9aqv4kguai26265393h9.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.906784991953-aoq1tgiac6ge9aqv4kguai26265393h9 GOOGLE_APP_ID 1:982053776531:ios:eec4ee1692d95d2ce3b166 diff --git a/README.md b/README.md index 67da738..04669ae 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ CodeBuilder Admin is a mobile application built with React Native and Expo. This ```env GOOGLE_MAPS_API_KEY=your_google_maps_api_key + GOOGLE_WEB_CLIENT_ID=your_google_web_client_id FIREBASE_API_KEY=your_firebase_api_key FIREBASE_AUTH_DOMAIN=your_firebase_auth_domain FIREBASE_PROJECT_ID=your_firebase_project_id diff --git a/app.config.js b/app.config.js index 97e869a..736affe 100644 --- a/app.config.js +++ b/app.config.js @@ -1,152 +1,153 @@ -import dotenv from "dotenv"; -import withNotificationToolsReplace from "./plugins/test.cjs"; -import versionData from "./version.json"; -import { withPlugins } from "@expo/config-plugins"; -import { withXcodeProject } from "expo/config-plugins"; -import fs from "fs"; +import dotenv from 'dotenv'; +import withNotificationToolsReplace from './plugins/test.cjs'; +import versionData from './version.json'; +import { withPlugins } from '@expo/config-plugins'; +import { withXcodeProject } from 'expo/config-plugins'; +import fs from 'fs'; // Explicitly load the .env file dotenv.config(); // Add this new plugin const withIOSSounds = (config) => { - return withXcodeProject(config, async (cfg) => { - const xcodeProject = cfg.modResults; - const appName = "CodeBuilder Admin"; // Match your iOS project name - const soundFiles = fs.readdirSync(`./ios/${appName}/Sounds`); + return withXcodeProject(config, async (cfg) => { + const xcodeProject = cfg.modResults; + const appName = 'CodeBuilder Admin'; // Match your iOS project name + const soundFiles = fs.readdirSync(`./ios/${appName}/Sounds`); - soundFiles.forEach((file) => { - xcodeProject.addResourceFile({ - path: `${appName}/Sounds/${file}`, - group: "Resources", - }); - }); + soundFiles.forEach((file) => { + xcodeProject.addResourceFile({ + path: `${appName}/Sounds/${file}`, + group: 'Resources', + }); + }); - return cfg; - }); + return cfg; + }); }; module.exports = { - expo: { - name: "CodeBuilder Admin", - slug: "codebuilder", - version: versionData.version, // Using version from version.json - extra: { - buildDate: process.env.BUILD_DATE || new Date().toISOString(), - eas: { - projectId: "c382aeb5-b138-47fb-83b4-dc45ab02ce76", - }, - firebaseApiKey: process.env.FIREBASE_API_KEY, - firebaseAuthDomain: process.env.FIREBASE_AUTH_DOMAIN, - firebaseProjectId: process.env.FIREBASE_PROJECT_ID, - firebaseStorageBucket: process.env.FIREBASE_STORAGE_BUCKET, - firebaseMessagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, - firebaseAppId: process.env.FIREBASE_APP_ID, - }, - orientation: "portrait", - icon: "./assets/images/icon.png", - scheme: "codebuilder-admin", - userInterfaceStyle: "automatic", - newArchEnabled: true, - notification: { - icon: "./assets/images/icon.png", - color: "#ffffff", // Optional: Tint color for the icon - }, - splash: { - image: "./assets/images/splash-icon.png", - resizeMode: "contain", - backgroundColor: "#ffffff", - }, - ios: { - buildNumber: versionData.iosBuildNumber, // Using iOS build number from version.json - supportsTablet: true, - bundleIdentifier: "com.digitalnomad91.codebuilderadmin", - googleServicesFile: "./GoogleService-Info.plist", - entitlements: { - "aps-environment": "production", - }, - config: { - // Provide your Google Maps API key - googleMapsApiKey: "YOUR_IOS_GOOGLE_MAPS_API_KEY", - }, - }, - android: { - versionCode: versionData.androidVersionCode, // Using Android versionCode from version.json - adaptiveIcon: { - foregroundImage: "./assets/images/adaptive-icon.png", - backgroundColor: "#ffffff", - }, - package: "com.digitalnomad91.codebuilderadmin", - permissions: ["NOTIFICATIONS", "POST_NOTIFICATIONS", "READ_PHONE_STATE"], - googleServicesFile: "./google-services.json", - useNextNotificationsApi: true, - notification: { - icon: "./assets/images/icon.png", - color: "#ffffff", // Optional: Tint color for the icon - }, - config: { - googleMaps: { - apiKey: process.env.GOOGLE_MAPS_API_KEY, + expo: { + name: 'CodeBuilder Admin', + slug: 'codebuilder', + version: versionData.version, // Using version from version.json + extra: { + buildDate: process.env.BUILD_DATE || new Date().toISOString(), + eas: { + projectId: 'c382aeb5-b138-47fb-83b4-dc45ab02ce76', + }, + firebaseApiKey: process.env.FIREBASE_API_KEY, + firebaseAuthDomain: process.env.FIREBASE_AUTH_DOMAIN, + firebaseProjectId: process.env.FIREBASE_PROJECT_ID, + firebaseStorageBucket: process.env.FIREBASE_STORAGE_BUCKET, + firebaseMessagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, + firebaseAppId: process.env.FIREBASE_APP_ID, + googleWebClientId: process.env.GOOGLE_WEB_CLIENT_ID || process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID, }, - }, - // manifest: { - // application: { - // metaData: [ - // { - // "android:name": "com.google.firebase.messaging.default_notification_color", - // "android:resource": "@color/notification_icon_color", - // "tools:replace": "android:resource", - // }, - // ], - // }, - // }, - }, - web: { - bundler: "metro", - output: "web-build", - favicon: "./assets/images/favicon.png", - }, - plugins: [ - withNotificationToolsReplace, - [ - "expo-camera", - { - cameraPermission: "Allow $(PRODUCT_NAME) to access your camera", - microphonePermission: - "Allow $(PRODUCT_NAME) to access your microphone", - recordAudioAndroid: true, + orientation: 'portrait', + icon: './assets/images/icon.png', + scheme: 'codebuilder-admin', + userInterfaceStyle: 'automatic', + newArchEnabled: true, + notification: { + icon: './assets/images/icon.png', + color: '#ffffff', // Optional: Tint color for the icon + }, + splash: { + image: './assets/images/splash-icon.png', + resizeMode: 'contain', + backgroundColor: '#ffffff', + }, + ios: { + buildNumber: versionData.iosBuildNumber, // Using iOS build number from version.json + supportsTablet: true, + bundleIdentifier: 'com.digitalnomad91.codebuilderadmin', + googleServicesFile: './GoogleService-Info.plist', + entitlements: { + 'aps-environment': 'production', + }, + config: { + // Provide your Google Maps API key + googleMapsApiKey: 'YOUR_IOS_GOOGLE_MAPS_API_KEY', + }, + }, + android: { + versionCode: versionData.androidVersionCode, // Using Android versionCode from version.json + adaptiveIcon: { + foregroundImage: './assets/images/adaptive-icon.png', + backgroundColor: '#ffffff', + }, + package: 'com.digitalnomad91.codebuilderadmin', + permissions: ['NOTIFICATIONS', 'POST_NOTIFICATIONS', 'READ_PHONE_STATE'], + googleServicesFile: './google-services.json', + useNextNotificationsApi: true, + notification: { + icon: './assets/images/icon.png', + color: '#ffffff', // Optional: Tint color for the icon + }, + config: { + googleMaps: { + apiKey: process.env.GOOGLE_MAPS_API_KEY, + }, + }, + // manifest: { + // application: { + // metaData: [ + // { + // "android:name": "com.google.firebase.messaging.default_notification_color", + // "android:resource": "@color/notification_icon_color", + // "tools:replace": "android:resource", + // }, + // ], + // }, + // }, }, - ], - "expo-router", - [ - "expo-build-properties", - { - newArchEnabled: false, - ios: { - flipper: false, - useFrameworks: "static", - }, - android: { - compileSdkVersion: 35, - targetSdkVersion: 35, - buildToolsVersion: "35.0.0", - }, + web: { + bundler: 'metro', + output: 'web-build', + favicon: './assets/images/favicon.png', }, - ], - "@react-native-firebase/app", - "@react-native-firebase/messaging", - // Add the iOS sound plugin - [withIOSSounds], - [ - "expo-notifications", - { - sounds: ["./assets/sounds/notification.aiff"], + plugins: [ + withNotificationToolsReplace, + [ + 'expo-camera', + { + cameraPermission: 'Allow $(PRODUCT_NAME) to access your camera', + microphonePermission: 'Allow $(PRODUCT_NAME) to access your microphone', + recordAudioAndroid: true, + }, + ], + 'expo-router', + [ + 'expo-build-properties', + { + newArchEnabled: false, + ios: { + flipper: false, + useFrameworks: 'static', + }, + android: { + compileSdkVersion: 35, + targetSdkVersion: 35, + buildToolsVersion: '35.0.0', + }, + }, + ], + '@react-native-firebase/app', + '@react-native-firebase/messaging', + '@react-native-google-signin/google-signin', + // Add the iOS sound plugin + [withIOSSounds], + [ + 'expo-notifications', + { + sounds: ['./assets/sounds/notification.aiff'], + }, + ], + ], + experiments: { + typedRoutes: true, + reactCanary: true, }, - ], - ], - experiments: { - typedRoutes: true, - reactCanary: true, }, - }, }; diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 9c88bd2..677eb4e 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,74 +1,64 @@ -import FontAwesome from "@expo/vector-icons/FontAwesome"; -import { Link, Tabs } from "expo-router"; -import { Pressable, Image, StyleSheet } from "react-native"; +import FontAwesome from '@expo/vector-icons/FontAwesome'; +import { Link, Tabs } from 'expo-router'; +import { Pressable, Image, StyleSheet } from 'react-native'; -import Colors from "@/constants/Colors"; -import { useColorScheme } from "@/hooks/useColorScheme"; -import { useClientOnlyValue } from "@/hooks/useClientOnlyValue"; +import Colors from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import { useClientOnlyValue } from '@/hooks/useClientOnlyValue'; // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ -function TabBarIcon(props: { - name: React.ComponentProps["name"]; - color: string; -}) { - return ; +function TabBarIcon(props: { name: React.ComponentProps['name']; color: string }) { + return ; } export default function TabLayout() { - const colorScheme = useColorScheme(); - return ( - - , - headerRight: () => ( - - - {({ pressed }) => ( - - )} - - - ), - }} - /> - ( - - ), - }} - /> - , - }} - /> - , - }} - /> - - ); + const colorScheme = useColorScheme(); + return ( + + , + headerRight: () => ( + + + {({ pressed }) => ( + + )} + + + ), + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ); } diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 25e4a13..7e9b748 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,142 +1,135 @@ -import { - View, - Text, - Button, - ActivityIndicator, - StyleSheet, - Image, - ScrollView, -} from "react-native"; -import MapView, { Marker } from "react-native-maps"; -import { useLocation } from "@/hooks/useLocation"; -import LogViewer from "@/components/LogViewer"; -import BatteryInfo from "@/components/BatteryInfo"; -import { triggerLocalSampleNotification } from "@/utils/notifications.utils"; -import { usePushNotifications } from "@/hooks/usePushNotifications"; +import { View, Text, Button, ActivityIndicator, StyleSheet, Image, ScrollView } from 'react-native'; +import MapView, { Marker } from 'react-native-maps'; +import { Link } from 'expo-router'; +import { useLocation } from '@/hooks/useLocation'; +import LogViewer from '@/components/LogViewer'; +import BatteryInfo from '@/components/BatteryInfo'; +import { triggerLocalSampleNotification } from '@/utils/notifications.utils'; +import { usePushNotifications } from '@/hooks/usePushNotifications'; export default function LocationComponent() { - const { fcmToken } = usePushNotifications(); - const { location, address, error, loading, fetchLocation } = - useLocation(fcmToken); + const { fcmToken } = usePushNotifications(); + const { location, address, error, loading, fetchLocation } = useLocation(fcmToken); - const textColor = "#ffffff"; + const textColor = '#ffffff'; - return ( - - - + return ( + + + - + - {/* Battery information */} - + {/* Battery information */} + - {/* Location / Map */} - {loading ? ( - - ) : error ? ( - {error} - ) : location && address ? ( - <> - - Address: {address.name}, {address.city}, {address.region},{" "} - {address.country} - - - {location.coords.latitude} - {location.coords.longitude} - + {/* Location / Map */} + {loading ? ( + + ) : error ? ( + {error} + ) : location && address ? ( + <> + + Address: {address.name}, {address.city}, {address.region}, {address.country} + + + {location.coords.latitude} - {location.coords.longitude} + - - - {location && ( - + + + {location && ( + + )} + + + + ) : ( + Waiting for location... )} - - - - ) : ( - - Waiting for location... - - )} -