Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ android {
applicationId 'com.aerostaffpro.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 104
versionName "2.7.28"
versionCode 105
versionName "2.7.29"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "AeroStaff Pro",
"slug": "AeroStaffPro",
"version": "2.7.28",
"version": "2.7.29",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Sync package version with native bump

This bumps Expo/Android to 2.7.29 while package.json, README.md, and src/utils/updateChecker.ts remain at 2.7.28 in this commit. scripts/release-check.cjs requires app.json and Android versionName to match package.json, so the release check will fail, and UI paths that import package.json still show the old version.

Useful? React with 👍 / 👎.

"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
Expand Down
21 changes: 11 additions & 10 deletions src/screens/FlightScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { requestShiftWidgetUpdate } from '../widgets/widgetThemeSync';
import { useLanguage } from '../context/LanguageContext';
import type { TranslationKey } from '../i18n/translations';
import { dismissPinnedFlightNotification, showOrUpdatePinnedFlightNotification } from '../utils/pinnedFlightOngoingNotification';
import { getBestArrivalTs, getBestDepartureTs } from '../utils/flightTimes';
import { getBestArrivalTs, getBestDepartureTs, getScheduledFlightTs } from '../utils/flightTimes';
import { isFlightEasyJet } from '../utils/easyjetOverlapMode';
import {
compareFlightsChronologically,
Expand Down Expand Up @@ -894,17 +894,18 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean
const wAllowedAirlines: string[] = wFilterRaw ? JSON.parse(wFilterRaw) : [];
const wFlights: WidgetFlight[] = mergedDeps
.filter(item => {
const ts = getBestDepartureTs(item);
if (ts == null) return false;
const stdTs = getScheduledFlightTs(item, 'departure');
if (stdTs == null) return false;
const airline = item.flight?.airline?.name || '';
if (wAllowedAirlines.length > 0 && !wAllowedAirlines.some(k => isFlightAirlineMatch(item, k))) return false;
const ops = getAirlineOps(airline);
const ciO = ts - ops.checkInOpen * 60, ciC = ts - ops.checkInClose * 60;
const gO = ts - ops.gateOpen * 60, gC = ts - ops.gateClose * 60;
const ciO = stdTs - ops.checkInOpen * 60, ciC = stdTs - ops.checkInClose * 60;
const gO = stdTs - ops.gateOpen * 60, gC = stdTs - ops.gateClose * 60;
return (ciO <= activeWidgetShift.end && ciC >= activeWidgetShift.start) || (gO <= activeWidgetShift.end && gC >= activeWidgetShift.start);
})
.map(item => {
const ts = getBestDepartureTs(item)!;
const etdTs = getBestDepartureTs(item)!;
const stdTs = getScheduledFlightTs(item, 'departure') ?? etdTs;
const airline = item.flight?.airline?.name || 'Sconosciuta';
const airlineIdentity = [
airline,
Expand All @@ -921,10 +922,10 @@ export default function FlightScreen({ isFocused = true }: { isFocused?: boolean
return {
flightNumber: fn,
destinationIata: getFlightAirportLabel(item.flight?.airport?.destination, 'N/A'),
departureTs: ts,
departureTime: fmtT(ts),
ciOpen: fmtOff(ts, ops.checkInOpen), ciClose: fmtOff(ts, ops.checkInClose),
gateOpen: fmtOff(ts, ops.gateOpen), gateClose: fmtOff(ts, ops.gateClose),
departureTs: etdTs,
departureTime: fmtT(etdTs),
ciOpen: fmtOff(stdTs, ops.checkInOpen), ciClose: fmtOff(stdTs, ops.checkInClose),
gateOpen: fmtOff(stdTs, ops.gateOpen), gateClose: fmtOff(stdTs, ops.gateClose),
airlineColor: getAirlineColor(airlineIdentity),
isPinned: fn === pinnedFn,
stand: sm?.stand,
Expand Down
35 changes: 18 additions & 17 deletions src/utils/autoNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { getAirlineOps } from './airlineOps';
import { fetchAirportScheduleRaw } from './fr24api';
import { getFlightAirportLabel, isFlightAirlineMatch } from './flightScheduleAdapter';
import { getBestArrivalTs, getBestDepartureTs } from './flightTimes';
import { getBestArrivalTs, getBestDepartureTs, getScheduledFlightTs } from './flightTimes';
import {
showShiftOngoingNotification,
dismissShiftOngoingNotification,
Expand Down Expand Up @@ -237,22 +237,23 @@ export async function autoScheduleNotifications(): Promise<number> {
// ── Departure notifications: check-in/gate open-close warnings ──
for (const item of shiftDepartures) {
try {
const depTs = getBestDepartureTs(item);
if (!depTs || isNaN(depTs)) continue;
const etdTs = getBestDepartureTs(item);
const stdTs = getScheduledFlightTs(item, 'departure') ?? etdTs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Select closure notifications by scheduled time

Because shiftDepartures is still built from getBestDepartureTs above, this new scheduled-time anchoring never runs for a delayed departure whose ETD moves after the shift even though its STD-based CI/gate close warnings fall during the shift. In that scenario the startup scheduler silently skips all CI/gate notifications for the flight; filter departures by the scheduled/ops overlap before scheduling these STD-based warnings.

Useful? React with 👍 / 👎.

if (!stdTs || isNaN(stdTs)) continue;

const airline = item.flight?.airline?.name || 'Sconosciuta';
const flightNumber = item.flight?.identification?.number?.default || 'N/A';
const destination = getFlightAirportLabel(item.flight?.airport?.destination, 'N/A');
const depTime = new Date(depTs * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
const depTime = new Date((etdTs ?? stdTs) * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });

// Get airline-specific ops times
const ops = getAirlineOps(airline);

// Check-in open/close timestamps
const ciOpenTs = depTs - ops.checkInOpen * 60;
const ciCloseTs = depTs - ops.checkInClose * 60;
const gateOpenTs = depTs - ops.gateOpen * 60;
const gateCloseTs = depTs - ops.gateClose * 60;
// Closure timestamps anchored to scheduled departure — never shift with delays
const ciOpenTs = stdTs - ops.checkInOpen * 60;
const ciCloseTs = stdTs - ops.checkInClose * 60;
const gateOpenTs = stdTs - ops.gateOpen * 60;
const gateCloseTs = stdTs - ops.gateClose * 60;

// Notification 10 min before check-in open
const secondsUntilCIOpenWarn = ciOpenTs - 10 * 60 - now;
Expand All @@ -267,8 +268,8 @@ export async function autoScheduleNotifications(): Promise<number> {
scheduler: 'auto',
type: 'checkin_open_10min',
flightNumber,
ts: depTs,
extra: { depTs },
ts: stdTs,
extra: { depTs: stdTs },
}),
},
trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilCIOpenWarn), repeats: false },
Expand All @@ -289,8 +290,8 @@ export async function autoScheduleNotifications(): Promise<number> {
scheduler: 'auto',
type: 'checkin_close_10min',
flightNumber,
ts: depTs,
extra: { depTs },
ts: stdTs,
extra: { depTs: stdTs },
}),
},
trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilCICloseWarn), repeats: false },
Expand All @@ -311,8 +312,8 @@ export async function autoScheduleNotifications(): Promise<number> {
scheduler: 'auto',
type: 'gate_open_5min',
flightNumber,
ts: depTs,
extra: { depTs },
ts: stdTs,
extra: { depTs: stdTs },
}),
},
trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilGateOpenWarn), repeats: false },
Expand All @@ -333,8 +334,8 @@ export async function autoScheduleNotifications(): Promise<number> {
scheduler: 'auto',
type: 'gate_close_5min',
flightNumber,
ts: depTs,
extra: { depTs },
ts: stdTs,
extra: { depTs: stdTs },
}),
},
trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilGateCloseWarn), repeats: false },
Expand Down
24 changes: 14 additions & 10 deletions src/utils/flightNotificationScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Notifications from 'expo-notifications';
import { getAirlineOps } from './airlineOps';
import { getFlightAirportLabel } from './flightScheduleAdapter';
import { getBestArrivalTs, getBestDepartureTs } from './flightTimes';
import { getBestArrivalTs, getBestDepartureTs, getScheduledFlightTs } from './flightTimes';
import { shouldNotifyAirline, type FlightNotificationSettings } from './flightNotificationSettings';
import { isFlightEasyJet } from './easyjetOverlapMode';
import {
Expand Down Expand Up @@ -229,26 +229,30 @@ export async function schedulePinnedNotifications(
ids.push(id);
}
} else {
const ts = getBestDepartureTs(item);
if (!ts) return;
const etdTs = getBestDepartureTs(item);
if (!etdTs) return;
const stdTs = getScheduledFlightTs(item, 'departure') ?? etdTs;
const dest = getFlightAirportLabel(item.flight?.airport?.destination, 'N/A');
const depTime = new Date(ts * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
const depTime = new Date(etdTs * 1000).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
const ops = getAirlineOps(airline);

const phases: Array<{ offset: number; type: string; title: string; body: string }> = [
{ offset: ops.checkInOpen, type: 'pinned_checkin_open', title: `Check-in aperto - ${flightNumber}`, body: `Check-in aperto per il volo delle ${depTime} → ${dest}` },
{ offset: ops.gateOpen, type: 'pinned_gate_open', title: `Gate aperto - ${flightNumber}`, body: `Gate aperto per il volo delle ${depTime} → ${dest}` },
{ offset: ops.gateClose, type: 'pinned_gate_close', title: `Chiusura gate - ${flightNumber}`, body: `Gate in chiusura per il volo delle ${depTime} → ${dest}` },
// Closure phases use stdTs so they never shift when the flight is delayed.
// Only the departure notification uses etdTs (the real/estimated departure).
const phases: Array<{ offset: number; type: string; title: string; body: string; baseTs: number }> = [
{ offset: ops.checkInOpen, type: 'pinned_checkin_open', title: `Check-in aperto - ${flightNumber}`, body: `Check-in aperto per il volo delle ${depTime} → ${dest}`, baseTs: stdTs },
{ offset: ops.gateOpen, type: 'pinned_gate_open', title: `Gate aperto - ${flightNumber}`, body: `Gate aperto per il volo delle ${depTime} → ${dest}`, baseTs: stdTs },
{ offset: ops.gateClose, type: 'pinned_gate_close', title: `Chiusura gate - ${flightNumber}`, body: `Gate in chiusura per il volo delle ${depTime} → ${dest}`, baseTs: stdTs },
{
offset: settings.departureLeadMinutes,
type: 'pinned_departure',
title: `Partenza tra ${settings.departureLeadMinutes} min - ${flightNumber}`,
body: `${airline} → ${dest} · partenza alle ${depTime}`,
baseTs: etdTs,
},
];

for (const phase of phases) {
const secsUntil = ts - phase.offset * 60 - now;
const secsUntil = phase.baseTs - phase.offset * 60 - now;
if (secsUntil <= 0) continue;
const id = await Notifications.scheduleNotificationAsync({
content: {
Expand All @@ -261,7 +265,7 @@ export async function schedulePinnedNotifications(
scheduler: 'flights_pinned',
type: phase.type,
flightNumber,
ts,
ts: phase.baseTs,
pinned: true,
}),
},
Expand Down
Loading