diff --git a/android/app/build.gradle b/android/app/build.gradle index ce16aa73..125de5e1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -120,8 +120,8 @@ android { applicationId "com.betterrail" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 55 - versionName "2.0.2" + versionCode 66 + versionName "2.1.0" missingDimensionStrategy "store", "play" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index aaff46e6..4ccf3256 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + diff --git a/android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml b/android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml new file mode 100644 index 00000000..bfe26435 --- /dev/null +++ b/android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/app/src/main/res/drawable-hdpi/notification_icon.png b/android/app/src/main/res/drawable-hdpi/notification_icon.png new file mode 100644 index 00000000..21e0bb51 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/notification_icon.png differ diff --git a/android/app/src/main/res/drawable-mdpi/notification_icon.png b/android/app/src/main/res/drawable-mdpi/notification_icon.png new file mode 100644 index 00000000..e14d80aa Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/notification_icon.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/notification_icon.png b/android/app/src/main/res/drawable-xhdpi/notification_icon.png new file mode 100644 index 00000000..2daf2cc2 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/notification_icon.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/notification_icon.png b/android/app/src/main/res/drawable-xxhdpi/notification_icon.png new file mode 100644 index 00000000..d45e27d8 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/notification_icon.png differ diff --git a/app/app.tsx b/app/app.tsx index e61f796d..9eff4b77 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -12,7 +12,7 @@ import "./i18n" import "./utils/ignore-warnings" import React, { useState, useEffect, useRef } from "react" -import { AppState } from "react-native" +import { AppState, Platform } from "react-native" import { QueryClient, QueryClientProvider } from "react-query" import { NavigationContainerRef } from "@react-navigation/native" import { SafeAreaProvider, initialWindowMetrics } from "react-native-safe-area-context" @@ -36,6 +36,7 @@ import { ToggleStorybook } from "../storybook/toggle-storybook" import { setInitialLanguage, setUserLanguage } from "./i18n/i18n" import "react-native-console-time-polyfill" import { withIAPContext } from "react-native-iap" +import PushNotification from "react-native-push-notification" // Disable tracking in development environment if (__DEV__) { @@ -49,6 +50,7 @@ if (__DEV__) { import { enableScreens } from "react-native-screens" import { canRunLiveActivities, monitorLiveActivities } from "./utils/ios-helpers" import { useDeepLinking } from "./hooks/use-deep-linking" +import { openActiveRide } from "./utils/helpers/ride-helpers" enableScreens() export const queryClient = new QueryClient() @@ -72,6 +74,18 @@ function App() { } }, []) + useEffect(() => { + if (Platform.OS === "android") { + PushNotification.configure({ + onNotification(notification) { + if (notification.userInteraction) { + openActiveRide(rootStore, navigationRef) + } + }, + }) + } + }, [rootStore, navigationRef]) + useEffect(() => { // Refresh app state when app is opened from background const subscription = AppState.addEventListener("change", (nextAppState) => { diff --git a/app/components/route-details-header/route-details-header.tsx b/app/components/route-details-header/route-details-header.tsx index 7eb1632e..e954693d 100644 --- a/app/components/route-details-header/route-details-header.tsx +++ b/app/components/route-details-header/route-details-header.tsx @@ -1,6 +1,17 @@ /* eslint-disable react/display-name */ import React, { useMemo, useLayoutEffect, useRef, useEffect } from "react" -import { Image, ImageBackground, View, ViewStyle, TextStyle, ImageStyle, Alert, Linking, Animated } from "react-native" +import { + Image, + ImageBackground, + View, + ViewStyle, + TextStyle, + ImageStyle, + Alert, + Linking, + Animated, + TouchableOpacity, +} from "react-native" import TouchableScale from "react-native-touchable-scale" import analytics from "@react-native-firebase/analytics" import { useNavigation } from "@react-navigation/native" @@ -16,7 +27,6 @@ import * as Burnt from "burnt" import * as AddCalendarEvent from "react-native-add-calendar-event" import { CalendarIcon } from "../calendar-icon/calendar-icon" import { RouteItem } from "../../services/api" -import { TouchableOpacity } from "react-native-gesture-handler" const AnimatedTouchable = Animated.createAnimatedComponent(TouchableScale) @@ -151,12 +161,10 @@ export const RouteDetailsHeader = observer(function RouteDetailsHeader(props: Ro } useEffect(() => { - if (routePlan.origin.id !== routePlan.destination.id) { - navigation.setParams({ - originId: routePlan.origin.id, - destinationId: routePlan.destination.id, - } as any) - } + navigation.setParams({ + originId: routePlan.origin.id, + destinationId: routePlan.destination.id, + } as any) }, [routePlan.origin.id, routePlan.destination.id]) const scaleStationCards = () => { diff --git a/app/hooks/use-ride-progress/index.ts b/app/hooks/use-ride-progress/index.ts index c5bc36be..f6c4821f 100644 --- a/app/hooks/use-ride-progress/index.ts +++ b/app/hooks/use-ride-progress/index.ts @@ -3,3 +3,4 @@ export * from "./use-ride-progress-animation" export * from "./use-ride-route" export * from "./use-ride-status" export * from "./get-stop-stations-status" +export * from "./utils" diff --git a/app/hooks/use-ride-progress/use-ride-progress.ts b/app/hooks/use-ride-progress/use-ride-progress.ts index 48cdd4ca..3e4b1232 100644 --- a/app/hooks/use-ride-progress/use-ride-progress.ts +++ b/app/hooks/use-ride-progress/use-ride-progress.ts @@ -1,9 +1,8 @@ import { useEffect, useState } from "react" -import { AppState, AppStateStatus } from "react-native" -import { addMinutes, differenceInMinutes } from "date-fns" +import { AppState } from "react-native" +import { differenceInMinutes } from "date-fns" import { RouteItem } from "../../services/api" -import { getPreviousTrainFromStationId, getTrainFromStationId } from "../../utils/helpers/ride-helpers" -import { useRideRoute, getStopStationStatus, useRideStatus } from "./" +import { useRideRoute, getStopStationStatus, useRideStatus, getStatusEndDate } from "./" export type RideStatus = "waitForTrain" | "inTransit" | "inExchange" | "arrived" @@ -14,21 +13,11 @@ export function useRideProgress({ route, enabled }: { route: RouteItem; enabled: const stations = getStopStationStatus({ route, nextStationId, status, enabled }) const calculateMinutesLeft = () => { - let date: Date - const train = getTrainFromStationId(route, nextStationId) - - const departureDate = addMinutes(train.departureTime, delay) - const arrivalDate = addMinutes(train.arrivalTime, delay) - - if (status === "waitForTrain" || status === "inExchange") { - date = departureDate - } else if (status === "inTransit" && train.originStationId === nextStationId && departureDate.getTime() > Date.now()) { - const previousTrain = getPreviousTrainFromStationId(route, nextStationId) ?? train - date = addMinutes(previousTrain.arrivalTime, delay) - } else { - date = arrivalDate - } - + const date = getStatusEndDate(route, { + delay, + status, + nextStationId, + }) setMinutesLeft(differenceInMinutes(date, Date.now(), { roundingMethod: "ceil" })) } diff --git a/app/hooks/use-ride-progress/utils.ts b/app/hooks/use-ride-progress/utils.ts new file mode 100644 index 00000000..b55cc6d9 --- /dev/null +++ b/app/hooks/use-ride-progress/utils.ts @@ -0,0 +1,56 @@ +import { addMinutes } from "date-fns" +import { RouteItem } from "../../services/api" +import { getTrainFromStationId, getPreviousTrainFromStationId } from "../../utils/helpers/ride-helpers" +import { RideStatus } from "./use-ride-progress" + +export type RideState = { + status: RideStatus + delay: number + nextStationId: number +} + +/** + * Returns the end date for the current ride state. + */ +export const getStatusEndDate = (route: RouteItem, state: RideState) => { + const train = getTrainFromStationId(route, state.nextStationId) + const departureDate = addMinutes(train.departureTime, state.delay) + const arrivalDate = addMinutes(train.arrivalTime, state.delay) + + if (state.status === "waitForTrain" || state.status === "inExchange") { + return departureDate + } else if ( + state.status === "inTransit" && + train.originStationId == state.nextStationId && + departureDate.getTime() > Date.now() + ) { + const previousTrain = getPreviousTrainFromStationId(route, state.nextStationId) ?? train + return addMinutes(previousTrain.arrivalTime, state.delay) + } else { + return arrivalDate + } +} + +/** + * Get information about the current ride progress + * @returns A tuple made out of the (current stop station index, total stop station count) + */ +export const rideProgress = (route: RouteItem, nextStationId: number) => { + const train = getTrainFromStationId(route, nextStationId) + if (!train) return [0, 0] + + const totalStations = train.stopStations.length + 1 + const currentIndex = train.stopStations.findIndex((station) => station.stationId === nextStationId) + if (currentIndex >= 0) { + return [currentIndex, totalStations] + } else { + for (let index = 0; index < route.trains.length; index++) { + if (nextStationId === route.trains[index].destinationStationId) { + let stopStationsCount = route.trains[index].stopStations.length + 1 + return [stopStationsCount - 1, stopStationsCount] + } + } + + return [0, 0] + } +} diff --git a/app/i18n/ar.json b/app/i18n/ar.json index a4b5acd8..0620683b 100644 --- a/app/i18n/ar.json +++ b/app/i18n/ar.json @@ -138,12 +138,15 @@ "live": "Live", "startRide": "ابدأ الركوب", "stopRide": "توقف عن الركوب", + "startNewRide": "ابدأ رحلة جديدة", "rideInFutureAlert": "يمكنك بدء رحلة جديدة فقط قبل ساعة من وقت مغادرة القطار.", "rideInPastAlert": "انتهت هذه الرحلة.", "rideExistsTitle": "يوجد رحلة نشطة", - "rideExistsMessage": "يرجى إلغاء الرحلة الحالية لبدء رحلة جديدة", - "disabledTitle": "Live Activities باطل", - "disabledMessage": "افتح الإعدادات وقم بتمكين Live Activities لتكون قادرًا على بدء مشوار", + "rideExistsMessage": "سيؤدي بدء رحلة جديدة إلى إلغاء الرحلة الحالية", + "liveActivitiesDisabledTitle": "Live Activities باطل", + "liveActivitiesDisabledMessage": "افتح الإعدادات وقم بتمكين Live Activities لتكون قادرًا على بدء مشوار", + "notificationsDisabledTitle": "الإخطارات تعطيل", + "notificationsDisabledMessage": "لبدء مشوار ، يرجى فتح إعدادات جهازك وتمكين الإخطارات.", "firstRideAlertP1": "During the first three days of usage, there is a possibility that the live activity feature may display an incorrect estimated time of arrival (ETA).", "firstRideAlertP2": "This is due to the system taking time to allocate sufficient \"notification budget\" for the application. ", diff --git a/app/i18n/en.json b/app/i18n/en.json index bcacd482..2f1d319d 100644 --- a/app/i18n/en.json +++ b/app/i18n/en.json @@ -43,6 +43,7 @@ "changes": "Changes", "delayTime": "minutes delay", "noTrainsFound": "No trains were found for this route in the upcoming days", + "sameStationsMessage": "Choose different origin and destination stations", "noInternetConnection": "It looks like your device is not currently connected to the internet.\nPlease check your network connection and try again.", "updates": "Updates from Israel Railways", "shortRoute": "Short Route" @@ -157,16 +158,19 @@ "title": "Better Rail Live", "subtitle": "Real-time updates with", "description": "Notifications about train delays, arrival time, travel instructions and more - all on your device lock screen.", + "androidDescription": "Train delay notifications, arrival time, travel instructions and more.", "weMadeAGuide": "We prepared a short guide on using this feature, which will change the way you travel with Israel Railways." }, "startRide": { "title": "Start Ride", - "description": "To use Better Rail Live, select a train from the list, then click \"Start Ride\" at the bottom of the screen.", + "description": "To use Better Rail Live, select a train from the list, then press \"Start Ride\" at the bottom of the screen.", "description2": "The trip details screen will now display the train progress, remaining time, and delay updates." }, "liveActivity": { "title": "Live Activity", - "description": "After you've started a trip, it'll be added to the lock screen and will be automatically updated with the train progress and delays.", + "androidTitle": "Active Ride", + "description": "Once you've started a ride, it'll be added to the lock screen and will be automatically updated with the train progress and delays.", + "androidDescription": "Once you've started a ride, it'll be added to the notifications screen and will automatically be updated with the train progress and delays.", "tip": "Tip: You can start your trip up to an hour before the train's departure time. This allows you to determine if you need to hurry to catch the train or not." }, "dynamicIsland": { @@ -187,21 +191,28 @@ "ride": { "live": "Live", "startRide": "Start Ride", + "startNewRide": "Start New Ride", "stopRide": "Stop Ride", "activatingRide": "Starting ride...", "rideInFutureAlert": "You can start a new ride only from an hour before the train's departure time.", "rideInPastAlert": "This ride has ended.", - "activeRideAlert": "An active ride exists.\n\nCancel the current ride in order to start a new one.", "trainArriving": "Train arrives at the destination", "arrivingIn": "Arriving in %{minutes} min", "arrived": "You have arrived", "departsIn": "Train departs in %{minutes} min", "departsNow": "Train departs now", + "trainInfo": "Train %{trainNumber} to %{lastStop}, departs from Platform %{platform}", + "getOffNextStop": "Get off at the next stop", + "getOffInStops": "Get off in %{stopsLeft} stops", + "greeting": "Thanks for riding with Better Rail", + "rideExistsTitle": "An active ride exists", - "rideExistsMessage": "Cancel the current ride in order to start a new one", - "disabledTitle": "Live Activities disabled", - "disabledMessage": "To start a ride, please open your device settings and enable Live Activities.", + "rideExistsMessage": "Starting a new ride will cancel the current one", + "liveActivitiesDisabledTitle": "Live Activities disabled", + "liveActivitiesDisabledMessage": "To start a ride, please open your device settings and enable Live Activities.", + "notificationsDisabledTitle": "Notifications disabled", + "notificationsDisabledMessage": "To start a ride, please open your device settings and enable notifications.", "firstRideAlertP1": "During the first three days of usage, there is a possibility that the live activity feature may display an incorrect estimated time of arrival (ETA).", "firstRideAlertP2": "This is due to the system taking time to allocate sufficient \"notification budget\" for the application. ", diff --git a/app/i18n/he.json b/app/i18n/he.json index 28745e21..952ee19b 100644 --- a/app/i18n/he.json +++ b/app/i18n/he.json @@ -43,6 +43,7 @@ "changes": "החלפות", "delayTime": "דק' עיכוב", "noTrainsFound": "לא נמצאו רכבות למסלול זה בימים הקרובים", + "sameStationsMessage": "יש לבחור תחנות מוצא ויעד שונות", "noInternetConnection": "נראה שהמכשיר אינו מחובר לאינטרנט.\n אנא בדקו את חיבור הרשת ונסו שוב.", "updates": "עדכונים מרכבת ישראל", "shortRoute": "מסלול קצר" @@ -159,7 +160,8 @@ "announcement": { "title": "בטר רייל לייב", "subtitle": "הנסיעה ברכבת הולכת להשתנות עם", - "description": "קבלו התראות על איחורי רכבות, זמן הגעה, הוראות נסיעה ועוד - והכל על גבי מסך הנעילה של המכשיר.", + "description": "התראות על איחורי רכבות, זמן הגעה, הוראות נסיעה ועוד - והכל על גבי מסך הנעילה של המכשיר.", + "androidDescription": "התראות על איחורי רכבות, זמן הגעה, הוראות נסיעה ועוד.", "weMadeAGuide": "הכנו מדריך קצר על השימוש בפיצ׳ר שישנה את הדרך בה אתם נוסעים ברכבת." }, "startRide": { @@ -170,7 +172,9 @@ }, "liveActivity": { "title": "לייב אקטיביטי", + "androidTitle": "נסיעה פעילה", "description": "אחרי שהתחלתם נסיעה, היא תתווסף למסך הנעילה של המכשיר ותתעדכן אוטומטית עם איחורים והתקדמות הרכבת.", + "androidDescription": "אחרי שהתחלתם נסיעה, היא תתווסף למסך הנוטיפקציות ותתעדכן אוטומטית עם איחורים והתקדמות הרכבת.", "tip": "טיפ: אפשר להתחיל את הנסיעה עד שעה לפני שהרכבת יוצאת מהתחנה. ככה תוכלו לדעת אם יש צורך לרוץ על מנת לתפוס את הרכבת, או שאפשר להשאר רגועים." }, "dynamicIsland": { @@ -179,7 +183,7 @@ "description2": "נתוני הנסיעה יופיעו על גבי האי הדינאמי, אליו תוכלו לגשת מכל מקום במכשיר." }, "supportUs": { - "title": "היי, אנחנו גיא ומתן", + "title": "היי! אנחנו גיא ומתן", "description1": "אנחנו שני נוסעי רכבת עם אהבה גדולה לפיתוח אפליקציות.", "description3": "העבודה על בטר רייל כרוכה במאות שעות עבודה, ואנחנו משלמים מכיסינו הפרטי על מנת לממן את פיתוח האפליקצייה.", "description4": "אם בטר רייל משפרת לכם את חווית הנסיעה ברכבת, אנא תמכו בנו.", @@ -191,20 +195,28 @@ "ride": { "live": "נסיעה", "startRide": "התחלת נסיעה", + "startNewRide": "התחלת נסיעה חדשה", "stopRide": "הפסקת נסיעה", "activatingRide": "יוצאים לדרך...", "rideInFutureAlert": "ניתן להתחיל נסיעה החל משעה לפני זמן יציאת הרכבת.", "rideInPastAlert": "נסיעה זו כבר הסתיימה.", "trainArriving": "הרכבת מתקרבת ליעד", - "arrivingIn": "הגעה ליעד בעוד %{minutes} דק'", + "arrivingIn": "הגעה בעוד %{minutes} דק'", "arrived": "הגעת ליעד", "departsIn": "הרכבת יוצאת בעוד %{minutes} דק'", "departsNow": "הרכבת יוצאת עכשיו", + "trainInfo": "רכבת %{trainNumber} ל%{lastStop}, תצא מרציף %{platform}", + "getOffNextStop": "הגעה ליעד בתחנה הבאה", + "getOffInStops": "הגעה ליעד בעוד %{stopsLeft} תחנות", + "greeting": "תודה שנסעתם עם בטר רייל!", + "rideExistsTitle": "קיימת נסיעה פעילה", - "rideExistsMessage": "עצרו את הנסיעה הנוכחית על מנת להתחיל חדשה", - "disabledTitle": "פעילויות בזמן אמת כבויות", - "disabledMessage": "יש לאפשר פעילויות בזמן אמת בהגדרות המכשיר על מנת להתחיל נסיעה", + "rideExistsMessage": "התחלת נסיעה חדשה תבטל את הנוכחית", + "liveActivitiesDisabledTitle": "פעילויות בזמן אמת כבויות", + "liveActivitiesDisabledMessage": "יש לאפשר פעילויות בזמן אמת בהגדרות המכשיר על מנת להתחיל נסיעה", + "notificationsDisabledTitle": "התראות כבויות", + "notificationsDisabledMessage": "יש לאפשר התראות בהגדרות המכשיר על מנת להתחיל נסיעה", "firstRideAlertP1": "במהלך שלושת ימי השימוש הראשונים, קיימת אפשרות שזמן ההגעה שיוצג בלייב אקטיביטי (על מסך הנעילה) יהיה לא מדויק.", "firstRideAlertP2": "זאת בשל העובדה שלוקח למערכת זמן להקצות \"תקציב נוטיפיקציות\" מספק עבור האפליקציה.", diff --git a/app/i18n/i18n.ts b/app/i18n/i18n.ts index 1507a131..80294a5c 100644 --- a/app/i18n/i18n.ts +++ b/app/i18n/i18n.ts @@ -1,6 +1,7 @@ -import { I18nManager } from "react-native" +import { I18nManager, Platform } from "react-native" import RNRestart from "react-native-restart" import analytics from "@react-native-firebase/analytics" +import Preferences from "react-native-default-preference" import * as storage from "../utils/storage" import * as Localization from "expo-localization" @@ -25,18 +26,23 @@ export const deviceLocale = Localization.locale analytics().setUserProperties({ deviceLocale }) -export function setInitialLanguage() { +export function getInitialLanguage(): LanguageCode { if (Localization.locale.startsWith("he")) { - changeUserLanguage("he") + return "he" } else if (Localization.locale.startsWith("ar")) { - changeUserLanguage("ar") + return "ar" } else if (Localization.locale.startsWith("ru")) { - changeUserLanguage("ru") + return "ru" } else { - changeUserLanguage("en") + return "en" } } +export function setInitialLanguage() { + const languageCode = getInitialLanguage() + changeUserLanguage(languageCode) +} + export function changeUserLanguage(languageCode: LanguageCode) { storage.save("appLanguage", languageCode).then(() => { setUserLanguage(languageCode) @@ -68,6 +74,11 @@ export function setUserLanguage(languageCode: LanguageCode) { userLocale = languageCode i18n.locale = languageCode + + if (Platform.OS === "android") { + Preferences.set("userLocale", languageCode) + } + if ( ((languageCode === "he" || languageCode === "ar") && !isRTL) || ((languageCode === "en" || languageCode === "ru") && isRTL) diff --git a/app/i18n/ru.json b/app/i18n/ru.json index 6618ec85..8be750be 100644 --- a/app/i18n/ru.json +++ b/app/i18n/ru.json @@ -142,13 +142,16 @@ "ride": { "live": "Live", "startRide": "начать поездку", + "startNewRide": "Начать новую поездку", "stopRide": "Остановить поездку", "rideInFutureAlert": "Начать новую поездку можно только за час до отправления поезда.", "rideInPastAlert": "Эта поездка закончилась.", "rideExistsTitle": "Активная поездка существует", - "rideExistsMessage": "Отмените текущую поездку, чтобы начать новую", - "disabledTitle": "Live Activities отменен", - "disabledMessage": "Откройте настройки и включите Live Activities чтобы иметь возможность начать поездку", + "rideExistsMessage": "Начало новой поездки отменит текущую.", + "liveActivitiesDisabledTitle": "Live Activities отменен", + "liveActivitiesDisabledMessage": "Откройте настройки и включите Live Activities чтобы иметь возможность начать поездку", + "notificationsDisabledTitle": "Уведомления отключены", + "notificationsDisabledMessage": "Чтобы начать поездку, откройте настройки своего устройства и включите уведомления.", "firstRideAlertP1": "During the first three days of usage, there is a possibility that the live activity feature may display an incorrect estimated time of arrival (ETA).", "firstRideAlertP2": "This is due to the system taking time to allocate sufficient \"notification budget\" for the application. ", diff --git a/app/models/ride/ride.ts b/app/models/ride/ride.ts index cfefdb89..8a3e3e62 100644 --- a/app/models/ride/ride.ts +++ b/app/models/ride/ride.ts @@ -1,18 +1,25 @@ import { Instance, SnapshotOut, types } from "mobx-state-tree" import { omit } from "ramda" import { Platform } from "react-native" +import AndroidHelpers from "../../utils/android-helpers" import iOSHelpers, { ActivityAuthorizationInfo, canRunLiveActivities } from "../../utils/ios-helpers" import { trainRouteSchema } from "../train-routes/train-routes" import { RouteItem } from "../../services/api" +import { RouteApi } from "../../services/api/route-api" +import { head, last } from "lodash" +import { formatDateForAPI } from "../../utils/helpers/date-helpers" +import { addMinutes } from "date-fns" + +const routeApi = new RouteApi() const startRideHandler: (route: RouteItem) => Promise = Platform.select({ ios: iOSHelpers.startLiveActivity, - android: () => Promise.resolve(""), + android: AndroidHelpers.startRideNotifications, }) const endRideHandler: (routeId: string) => Promise = Platform.select({ ios: iOSHelpers.endLiveActivity, - android: () => Promise.resolve(true), + android: AndroidHelpers.endRideNotifications, }) /** @@ -77,7 +84,7 @@ export const RideModel = types this.checkActivityAuthorizationInfo() }, startRide(route: RouteItem) { - if (!canRunLiveActivities) return + if (Platform.OS === "ios" && !canRunLiveActivities) return this.setRideLoading(true) this.setRoute(route) @@ -88,20 +95,27 @@ export const RideModel = types this.setRideLoading(false) }) .catch(() => { + this.setRoute(undefined) + this.setRideId(undefined) this.setRideLoading(false) + + if (Platform.OS === "android") { + AndroidHelpers.cancelNotifications() + } + alert( "An error occured while starting the ride.\nIf the issue persists, please let us know!\n\n Our email: feedback@better-rail.co.il.", ) }) }, stopRide(rideId: string) { - if (!canRunLiveActivities) return + if (Platform.OS === "ios" && !canRunLiveActivities) return Promise.resolve() this.setRideLoading(true) this.setRideId(undefined) this.setRoute(undefined) - endRideHandler(rideId).then(() => { + return endRideHandler(rideId).then(() => { this.setRideLoading(false) }) }, @@ -124,6 +138,23 @@ export const RideModel = types this.stopRide(rideId) } }) + } else if (Platform.OS === "android") { + if (!self.route || !self.id) return + + const originId = head(self.route.trains).originStationId + const destinationId = last(self.route.trains).destinationStationId + const [date, time] = formatDateForAPI(self.route.departureTime) + + routeApi.getRoutes(originId.toString(), destinationId.toString(), date, time).then((routes) => { + const currentRouteTrains = self.route.trains.map((train) => train.trainNumber).join() + const currentRoute = routes.find( + (route) => currentRouteTrains === route.trains.map((train) => train.trainNumber).join(), + ) + + if (currentRoute && Date.now() >= addMinutes(currentRoute.arrivalTime, last(currentRoute.trains).delay).getTime()) { + this.stopRide(rideId) + } + }) } }, /** diff --git a/app/navigators/active-ride/active-ride-navigator.tsx b/app/navigators/active-ride/active-ride-navigator.tsx index b304030a..d701d818 100644 --- a/app/navigators/active-ride/active-ride-navigator.tsx +++ b/app/navigators/active-ride/active-ride-navigator.tsx @@ -3,6 +3,7 @@ import { createStackNavigator, StackScreenProps, TransitionPresets } from "@reac import { RouteDetailsScreen } from "../../screens" import { RouteItem } from "../../services/api" import { CloseButton } from "../../components" +import { Platform } from "react-native" export type ActiveRideList = { activeRide: { routeItem: RouteItem; originId: number; destinationId: number } @@ -20,7 +21,7 @@ export const ActiveRideNavigator = () => ( options={({ navigation }) => ({ headerTransparent: true, title: "", - headerStatusBarHeight: 16, + headerStatusBarHeight: Platform.OS === "ios" ? 16 : undefined, headerLeft: () => navigation.goBack()} iconStyle={{ tintColor: "white" }} />, ...TransitionPresets.ModalSlideFromBottomIOS, })} diff --git a/app/navigators/live-activity-announcement/live-activity-announcement-stack.tsx b/app/navigators/live-activity-announcement/live-activity-announcement-stack.tsx index 3237134a..2f822d34 100644 --- a/app/navigators/live-activity-announcement/live-activity-announcement-stack.tsx +++ b/app/navigators/live-activity-announcement/live-activity-announcement-stack.tsx @@ -1,16 +1,12 @@ import React from "react" import { createStackNavigator, StackScreenProps } from "@react-navigation/stack" -import { - LiveAnnouncementScreen, - StartRideAnnouncement, - ActivityAnnouncementScreen, - DynamicIslandScreen, - SupportUsScreen, -} from "../../screens" +import { StartRideAnnouncement, ActivityAnnouncementScreen, DynamicIslandScreen, SupportUsScreen } from "../../screens" import { BlurView } from "@react-native-community/blur" import { useSafeAreaInsets } from "react-native-safe-area-context" import { CloseButton } from "../../components" import { useIsDarkMode } from "../../hooks" +import { Platform } from "react-native" +import { LiveAnnouncementScreen } from "../../screens/live-announcement/live-announcement-screen" export type LiveAnnouncementParamList = { main: undefined @@ -28,14 +24,25 @@ export const LiveAnnouncementNavigator = () => ( ({ headerTransparent: true, - headerLeft: () => ( - navigation.navigate("planner")} - iconStyle={{ width: 32.5, height: 32.5, tintColor: "white", opacity: 0.5, marginTop: 8 }} - /> - ), - headerBackground: () => , + headerBackground: () => (Platform.OS === "ios" ? : null), title: "", + ...(Platform.OS === "ios" && { + headerLeft: () => { + if (Platform.OS === "android") return null + return ( + navigation.navigate("planner")} + iconStyle={{ + width: 32.5, + height: 32.5, + tintColor: Platform.select({ ios: "white", android: "grey" }), + opacity: 0.5, + marginTop: 8, + }} + /> + ) + }, + }), })} > diff --git a/app/screens/live-announcement/activity-announcement-screen.android.tsx b/app/screens/live-announcement/activity-announcement-screen.android.tsx new file mode 100644 index 00000000..d3fe7162 --- /dev/null +++ b/app/screens/live-announcement/activity-announcement-screen.android.tsx @@ -0,0 +1,52 @@ +import { Image, ImageStyle, ScrollView, TextStyle, View } from "react-native" +import { Button, Text } from "../../components" +import { color, spacing } from "../../theme" +import { translate, userLocale } from "../../i18n" +import { LiveAnnouncementStackProps } from "../../navigators/live-activity-announcement/live-activity-announcement-stack" + +const TITLE: TextStyle = { + fontSize: 30, + fontWeight: "bold", + textAlign: "center", + marginBottom: spacing[2], +} +const TEXT: TextStyle = { fontSize: 22, textAlign: "center", lineHeight: 29 } + +const LIVE_ACTIVITY_IMAGE: ImageStyle = { + width: "100%", + height: 355, + resizeMode: "contain", + marginVertical: spacing[4], +} + +const NOTIFICATION_IMAGE_HEBREW = require("../../../assets/live-ride/live-ride-notification.png") +const NOTIFICATION_IMAGE_ENGLISH = require("../../../assets/live-ride/live-ride-notification-english.png") + +export function ActivityAnnouncementScreen({ navigation }: LiveAnnouncementStackProps) { + const NOTIFICATION_IMAGE = userLocale === "he" ? NOTIFICATION_IMAGE_HEBREW : NOTIFICATION_IMAGE_ENGLISH + + return ( + + + + + + + + +