Merge branch 'master' into release

This commit is contained in:
Nawaz Dhandala
2026-02-24 10:46:25 +00:00
6 changed files with 363 additions and 156 deletions

View File

@@ -16,6 +16,12 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.oneuptime.oncall",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#0D1117"
},
"infoPlist": {
"UIBackgroundModes": [
"remote-notification"
@@ -43,7 +49,17 @@
{
"backgroundColor": "#0D1117",
"image": "./assets/splash-icon.png",
"imageWidth": 200
"imageWidth": 200,
"ios": {
"backgroundColor": "#0D1117",
"image": "./assets/splash-icon.png",
"imageWidth": 200
},
"android": {
"backgroundColor": "#0D1117",
"image": "./assets/splash-icon.png",
"imageWidth": 200
}
}
],
[

View File

@@ -91,7 +91,14 @@ export async function requestPermissionsAndGetToken(): Promise<string | null> {
let finalStatus: PermissionStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
const { status } = await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowSound: true,
allowCriticalAlerts: true,
},
});
finalStatus = status;
}
@@ -115,10 +122,18 @@ export async function requestPermissionsAndGetToken(): Promise<string | null> {
}
try {
logger.info(
`[PushNotifications] Requesting Expo push token with projectId: ${projectId}`,
);
const tokenData: ExpoPushToken = await Notifications.getExpoPushTokenAsync({
projectId,
});
logger.info(
`[PushNotifications] Successfully obtained push token: ${tokenData.data}`,
);
return tokenData.data;
} catch (error: unknown) {
logger.error("[PushNotifications] Failed to get push token:", error);

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback, useMemo } from "react";
import {
View,
SectionList,
ScrollView,
RefreshControl,
Text,
SectionListRenderItemInfo,
@@ -307,19 +308,21 @@ export default function AlertsScreen(): React.JSX.Element {
<View
style={{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<View style={{ padding: 16 }}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</View>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<View style={{ padding: 16 }}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</View>
</ScrollView>
</View>
);
}
@@ -337,43 +340,48 @@ export default function AlertsScreen(): React.JSX.Element {
<View
style={{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<EmptyState
title="Something went wrong"
subtitle={
segment === "alerts"
? "Failed to load alerts. Pull to refresh or try again."
: "Failed to load alert episodes. Pull to refresh or try again."
}
icon="alerts"
actionLabel="Retry"
onAction={retryFn}
/>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<EmptyState
title="Something went wrong"
subtitle={
segment === "alerts"
? "Failed to load alerts. Pull to refresh or try again."
: "Failed to load alert episodes. Pull to refresh or try again."
}
icon="alerts"
actionLabel="Retry"
onAction={retryFn}
/>
</ScrollView>
</View>
);
}
return (
<View style={{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }}>
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
{segment === "alerts" ? (
<SectionList
sections={alertSections}
style={{ flex: 1 }}
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
}
keyExtractor={(wrapped: ProjectAlertItem) => {
return `${wrapped.projectId}-${wrapped.item._id}`;
}}
@@ -451,6 +459,17 @@ export default function AlertsScreen(): React.JSX.Element {
<SectionList
sections={episodeSections}
style={{ flex: 1 }}
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
}
keyExtractor={(wrapped: ProjectAlertEpisodeItem) => {
return `${wrapped.projectId}-${wrapped.item._id}`;
}}

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback, useMemo } from "react";
import {
View,
SectionList,
ScrollView,
RefreshControl,
Text,
SectionListRenderItemInfo,
@@ -312,19 +313,21 @@ export default function IncidentsScreen(): React.JSX.Element {
<View
style={{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<View style={{ padding: 16 }}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</View>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<View style={{ padding: 16 }}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</View>
</ScrollView>
</View>
);
}
@@ -342,43 +345,48 @@ export default function IncidentsScreen(): React.JSX.Element {
<View
style={{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<EmptyState
title="Something went wrong"
subtitle={
segment === "incidents"
? "Failed to load incidents. Pull to refresh or try again."
: "Failed to load incident episodes. Pull to refresh or try again."
}
icon="incidents"
actionLabel="Retry"
onAction={retryFn}
/>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
<EmptyState
title="Something went wrong"
subtitle={
segment === "incidents"
? "Failed to load incidents. Pull to refresh or try again."
: "Failed to load incident episodes. Pull to refresh or try again."
}
icon="incidents"
actionLabel="Retry"
onAction={retryFn}
/>
</ScrollView>
</View>
);
}
return (
<View style={{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }}>
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
{segment === "incidents" ? (
<SectionList
sections={incidentSections}
style={{ flex: 1 }}
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
}
keyExtractor={(wrapped: ProjectIncidentItem) => {
return `${wrapped.projectId}-${wrapped.item._id}`;
}}
@@ -457,6 +465,17 @@ export default function IncidentsScreen(): React.JSX.Element {
<SectionList
sections={episodeSections}
style={{ flex: 1 }}
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
{ key: "episodes" as const, label: "Episodes" },
]}
selected={segment}
onSelect={setSegment}
/>
}
keyExtractor={(wrapped: ProjectIncidentEpisodeItem) => {
return `${wrapped.projectId}-${wrapped.item._id}`;
}}

View File

@@ -81,7 +81,10 @@ export default function MyOnCallPoliciesScreen(): React.JSX.Element {
<View
style={{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }}
>
<ScrollView contentContainerStyle={{ padding: 16, paddingBottom: 44 }}>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{ padding: 16, paddingBottom: 44 }}
>
<View
style={{
borderRadius: 24,
@@ -123,6 +126,7 @@ export default function MyOnCallPoliciesScreen(): React.JSX.Element {
return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 20, paddingBottom: 56 }}
refreshControl={

View File

@@ -22,6 +22,7 @@ const APP_VERSION: string = "1.0.0";
interface SettingsRowProps {
label: string;
value?: string;
valueBelowLabel?: string;
onPress?: () => void;
rightElement?: React.ReactNode;
destructive?: boolean;
@@ -32,6 +33,7 @@ interface SettingsRowProps {
function SettingsRow({
label,
value,
valueBelowLabel,
onPress,
rightElement,
destructive,
@@ -43,11 +45,9 @@ function SettingsRow({
const content: React.JSX.Element = (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
minHeight: 52,
justifyContent: "center",
...(!isLast
? {
borderBottomWidth: 1,
@@ -56,57 +56,79 @@ function SettingsRow({
: {}),
}}
>
<View style={{ flexDirection: "row", alignItems: "center", flex: 1 }}>
{iconName ? (
<View
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<View style={{ flexDirection: "row", alignItems: "center", flex: 1 }}>
{iconName ? (
<View
style={{
width: 28,
height: 28,
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
marginRight: 12,
backgroundColor: destructive
? theme.colors.statusErrorBg
: theme.colors.iconBackground,
}}
>
<Ionicons
name={iconName}
size={15}
color={
destructive
? theme.colors.actionDestructive
: theme.colors.actionPrimary
}
/>
</View>
) : null}
<Text
style={{
width: 28,
height: 28,
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
marginRight: 12,
backgroundColor: destructive
? theme.colors.statusErrorBg
: theme.colors.iconBackground,
fontSize: 15,
fontWeight: "500",
paddingVertical: 12,
color: destructive
? theme.colors.actionDestructive
: theme.colors.textPrimary,
}}
>
{label}
</Text>
</View>
{rightElement ??
(value ? (
<Text style={{ fontSize: 14, color: theme.colors.textTertiary }}>
{value}
</Text>
) : onPress ? (
<Ionicons
name={iconName}
size={15}
color={
destructive
? theme.colors.actionDestructive
: theme.colors.actionPrimary
}
name="chevron-forward"
size={18}
color={theme.colors.textTertiary}
/>
</View>
) : null}
) : null)}
</View>
{valueBelowLabel ? (
<Text
style={{
fontSize: 15,
fontWeight: "500",
paddingVertical: 12,
color: destructive
? theme.colors.actionDestructive
: theme.colors.textPrimary,
fontSize: 13,
color: theme.colors.textTertiary,
paddingBottom: 12,
marginTop: -4,
}}
numberOfLines={1}
ellipsizeMode="tail"
>
{label}
{valueBelowLabel}
</Text>
</View>
{rightElement ??
(value ? (
<Text style={{ fontSize: 14, color: theme.colors.textTertiary }}>
{value}
</Text>
) : onPress ? (
<Ionicons
name="chevron-forward"
size={18}
color={theme.colors.textTertiary}
/>
) : null)}
) : null}
</View>
);
@@ -150,6 +172,7 @@ export default function SettingsScreen(): React.JSX.Element {
return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 20, paddingBottom: 120 }}
>
@@ -229,43 +252,70 @@ export default function SettingsScreen(): React.JSX.Element {
<View
style={{
marginTop: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
borderRadius: 10,
backgroundColor: theme.colors.backgroundTertiary,
borderWidth: 1,
borderColor: theme.colors.borderSubtle,
paddingHorizontal: 12,
paddingVertical: 10,
}}
>
<Text
style={{
fontSize: 11,
fontWeight: "600",
textTransform: "uppercase",
color: theme.colors.textTertiary,
letterSpacing: 1,
}}
>
Connected to
</Text>
<View
style={{
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
backgroundColor: theme.colors.accentCyanBg,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Text
style={{
fontSize: 11,
fontWeight: "600",
color: theme.colors.accentCyan,
textTransform: "uppercase",
color: theme.colors.textTertiary,
letterSpacing: 1,
}}
>
{serverUrl || "oneuptime.com"}
Connected to
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
}}
>
<View
style={{
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.statusSuccess,
marginRight: 5,
}}
/>
<Text
style={{
fontSize: 11,
fontWeight: "600",
color: theme.colors.statusSuccess,
}}
>
Online
</Text>
</View>
</View>
<Text
style={{
fontSize: 13,
fontWeight: "500",
color: theme.colors.textSecondary,
marginTop: 6,
}}
numberOfLines={1}
ellipsizeMode="middle"
>
{serverUrl || "oneuptime.com"}
</Text>
</View>
</View>
@@ -347,14 +397,98 @@ export default function SettingsScreen(): React.JSX.Element {
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
padding: 16,
}}
>
<SettingsRow
label="Server URL"
iconName="globe-outline"
value={serverUrl || "oneuptime.com"}
isLast
/>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 12,
}}
>
<View
style={{
width: 28,
height: 28,
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
marginRight: 10,
backgroundColor: theme.colors.iconBackground,
}}
>
<Ionicons
name="globe-outline"
size={15}
color={theme.colors.actionPrimary}
/>
</View>
<Text
style={{
fontSize: 15,
fontWeight: "500",
color: theme.colors.textPrimary,
flex: 1,
}}
>
Server URL
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 9999,
backgroundColor: theme.colors.statusSuccessBg,
}}
>
<View
style={{
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.statusSuccess,
marginRight: 5,
}}
/>
<Text
style={{
fontSize: 10,
fontWeight: "600",
color: theme.colors.statusSuccess,
letterSpacing: 0.2,
}}
>
Connected
</Text>
</View>
</View>
<View
style={{
borderRadius: 10,
backgroundColor: theme.colors.backgroundTertiary,
borderWidth: 1,
borderColor: theme.colors.borderSubtle,
paddingHorizontal: 12,
paddingVertical: 10,
}}
>
<Text
style={{
fontSize: 13,
fontWeight: "500",
color: theme.colors.textSecondary,
fontFamily: undefined,
}}
numberOfLines={1}
ellipsizeMode="middle"
>
{serverUrl || "oneuptime.com"}
</Text>
</View>
</View>
</View>