Skip to content
32 changes: 24 additions & 8 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ jobs:
echo "Prebuilding iOS project..."
npx expo prebuild --platform ios --clean --no-install

- name: Set iOS deployment target for pods
if: matrix.language == 'swift'
run: |
node -e "const fs=require('fs'); const p='ios/Podfile.properties.json'; const d=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{}; d['ios.deploymentTarget']='15.5'; fs.writeFileSync(p, JSON.stringify(d, null, 2) + '\n'); console.log('Configured ios.deploymentTarget=15.5 in ' + p);"

- name: Install iOS dependencies
if: matrix.language == 'swift'
run: |
Expand Down Expand Up @@ -166,14 +171,25 @@ jobs:
run: |
echo "Attempting fallback iOS build with minimal configuration..."
cd ios

# Try a minimal build using the same approach as main.yml
xcodebuild -workspace pulse.xcworkspace \
-scheme pulse \
-configuration Debug \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=18.2' \
build-for-testing

# Guard against missing workspace when pod install failed earlier.
if [ ! -d "pulse.xcworkspace" ]; then
echo "pulse.xcworkspace is missing; retrying pod install before fallback build..."
pod install --repo-update
fi

if [ ! -d "pulse.xcworkspace" ]; then
echo "❌ pulse.xcworkspace still missing after pod install. Fallback build cannot continue."
exit 1
fi

# Try a minimal build using the same approach as main.yml.
xcodebuild -workspace pulse.xcworkspace \
-scheme pulse \
-configuration Debug \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone SE (3rd generation),OS=18.2' \
build-for-testing

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ jobs:
echo "Prebuilding iOS project..."
npx expo prebuild --platform ios --clean

- name: Set iOS deployment target for pods
run: |
node -e "const fs=require('fs'); const p='ios/Podfile.properties.json'; const d=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{}; d['ios.deploymentTarget']='15.5'; fs.writeFileSync(p, JSON.stringify(d, null, 2) + '\n'); console.log('Configured ios.deploymentTarget=15.5 in ' + p);"

- name: Install iOS dependencies
run: |
cd ios
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/testflight-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ jobs:
- name: Prebuild iOS project
run: npx expo prebuild --clean

- name: Set iOS deployment target for pods
run: |
node -e "const fs=require('fs'); const p='ios/Podfile.properties.json'; const d=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{}; d['ios.deploymentTarget']='15.5'; fs.writeFileSync(p, JSON.stringify(d, null, 2) + '\n'); console.log('Configured ios.deploymentTarget=15.5 in ' + p);"

- name: Install iOS dependencies
run: |
cd ios
Expand Down
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "pulse",
"slug": "pulse",
"version": "1.1.2",
"version": "1.1.3",
"orientation": "portrait",
"icon": "./assets/images/pulse-logo.png",
"scheme": "pulsecam",
Expand Down Expand Up @@ -76,4 +76,4 @@
},
"owner": "morepriyam"
}
}
}
64 changes: 63 additions & 1 deletion app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@
ThemeProvider,
} from "@react-navigation/native";
import { useFonts } from "expo-font";
import { useRouter } from "expo-router";

Check warning on line 7 in app/_layout.tsx

View workflow job for this annotation

GitHub Actions / Lint and Test

'/home/runner/work/pulse/pulse/node_modules/expo-router/build/index.js' imported multiple times
import { Stack } from "expo-router";

Check warning on line 8 in app/_layout.tsx

View workflow job for this annotation

GitHub Actions / Lint and Test

'/home/runner/work/pulse/pulse/node_modules/expo-router/build/index.js' imported multiple times
import * as Linking from "expo-linking";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "react-native-reanimated";

import { PermissionMonitor } from "@/components/PermissionMonitor";
import { ReportIssueFab } from "@/components/ReportIssueFab";
import { ReportIssueModal } from "@/components/ReportIssueModal";
import { useColorScheme } from "@/hooks/useColorScheme";
import {
BUG_REPORT_NOTIFICATION_OPEN_ACTION,
openBugReportNotificationUrl,
} from "@/utils/localNotification";
import { storeUploadConfigForDraft } from "@/utils/uploadConfig";
import { addDestination } from "@/utils/uploadDestinations";

Expand All @@ -23,6 +29,7 @@
export default function RootLayout() {
const colorScheme = useColorScheme();
const router = useRouter();
const [isReportIssueModalVisible, setIsReportIssueModalVisible] = useState(false);
const [loaded] = useFonts({
"Roboto-Regular": require("../assets/fonts/Roboto-Regular.ttf"),
"Roboto-Bold": require("../assets/fonts/Roboto-Bold.ttf"),
Expand Down Expand Up @@ -112,6 +119,56 @@
};
}, [router]);

useEffect(() => {
let cancelled = false;
let subscription: { remove: () => void } | null = null;

const setupNotifications = async () => {
const Notifications = await import("expo-notifications");

const handleResponse = async (response: {
actionIdentifier: string;
notification: {
request: {
content: {
data?: {
issueUrl?: string | null;
};
};
};
};
}) => {
if (cancelled) return;

const issueUrl = response.notification.request.content.data?.issueUrl;
if (!issueUrl) return;

if (
response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER ||
response.actionIdentifier === BUG_REPORT_NOTIFICATION_OPEN_ACTION
) {
await openBugReportNotificationUrl(issueUrl);
}
};

subscription = Notifications.addNotificationResponseReceivedListener((response) => {
void handleResponse(response);
});

const lastResponse = await Notifications.getLastNotificationResponseAsync();
if (lastResponse) {
await handleResponse(lastResponse as never);
}
};

void setupNotifications();

return () => {
cancelled = true;
subscription?.remove();
};
}, []);

if (!loaded) {
// Async font loading only occurs in development.
return null;
Expand Down Expand Up @@ -184,6 +241,11 @@
/>
</Stack>
<StatusBar style="auto" />
<ReportIssueFab onPress={() => setIsReportIssueModalVisible(true)} />
<ReportIssueModal
visible={isReportIssueModalVisible}
onClose={() => setIsReportIssueModalVisible(false)}
/>
<PermissionMonitor />
</ThemeProvider>
</GestureHandlerRootView>
Expand Down
59 changes: 59 additions & 0 deletions components/ReportIssueFab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { usePathname } from "expo-router";
import * as React from "react";
import { Pressable, PressableStateCallbackType, StyleSheet } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

interface ReportIssueFabProps {
onPress: () => void;
}

export function ReportIssueFab({ onPress }: ReportIssueFabProps) {
const pathname = usePathname();
const insets = useSafeAreaInsets();
const keepLowerOffset =
pathname.includes("shorts") || pathname.includes("trim-segment");
const topOffset = keepLowerOffset
? insets.top + 104
: insets.top + 15;

return (
<Pressable
accessibilityLabel="Report an issue"
accessibilityRole="button"
hitSlop={8}
onPress={onPress}
style={({ pressed }: PressableStateCallbackType) => [
styles.button,
{
top: topOffset,
opacity: pressed ? 0.85 : 1,
transform: [{ scale: pressed ? 0.97 : 1 }],
},
]}
>
<MaterialIcons name="bug-report" size={22} color="#FFFFFF" />
</Pressable>
);
}

const styles = StyleSheet.create({
button: {
position: "absolute",
right: 25,
width: 40,
height: 40,
borderRadius: 25,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(0, 0, 0, 0.78)",
borderWidth: 1.5,
borderColor: "rgba(255, 255, 255, 0.65)",
zIndex: 950,
elevation: 10,
shadowColor: "#000",
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.4,
shadowRadius: 5,
},
});
Loading
Loading