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 (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/screens/live-announcement/activity-announcement-screen.tsx b/app/screens/live-announcement/activity-announcement-screen.ios.tsx
similarity index 100%
rename from app/screens/live-announcement/activity-announcement-screen.tsx
rename to app/screens/live-announcement/activity-announcement-screen.ios.tsx
diff --git a/app/screens/live-announcement/activity-announcement-screen.ts b/app/screens/live-announcement/activity-announcement-screen.ts
new file mode 100644
index 00000000..2a205c4d
--- /dev/null
+++ b/app/screens/live-announcement/activity-announcement-screen.ts
@@ -0,0 +1,9 @@
+import { Platform } from "react-native"
+
+import { ActivityAnnouncementScreen as ActivityAnnouncementScreenIOS } from "./activity-announcement-screen.ios"
+import { ActivityAnnouncementScreen as ActivityAnnouncementScreenAndroid } from "./activity-announcement-screen.android"
+
+export const ActivityAnnouncementScreen = Platform.select({
+ ios: ActivityAnnouncementScreenIOS,
+ android: ActivityAnnouncementScreenAndroid,
+})
diff --git a/app/screens/live-announcement/index.ts b/app/screens/live-announcement/index.ts
index 1ad70502..d9297560 100644
--- a/app/screens/live-announcement/index.ts
+++ b/app/screens/live-announcement/index.ts
@@ -1,5 +1,5 @@
export * from "./activity-announcement-screen"
-export * from "./start-ride-announcement-screen"
export * from "./live-announcement-screen"
+export * from "./start-ride-screen"
export * from "./dynamic-island-screen"
export * from "./support-us-announcement-screen"
diff --git a/app/screens/live-announcement/live-announcement-screen.android.tsx b/app/screens/live-announcement/live-announcement-screen.android.tsx
new file mode 100644
index 00000000..e5da6505
--- /dev/null
+++ b/app/screens/live-announcement/live-announcement-screen.android.tsx
@@ -0,0 +1,53 @@
+import { Image, ImageStyle, ScrollView, TextStyle, View } from "react-native"
+import { Button, Text } from "../../components"
+import { color, fontScale, spacing } from "../../theme"
+import { translate, userLocale } from "../../i18n"
+import { LiveAnnouncementStackProps } from "../../navigators/live-activity-announcement/live-activity-announcement-stack"
+
+const SUB_TITLE: TextStyle = {
+ fontSize: 24,
+ textAlign: "center",
+}
+const TITLE: TextStyle = {
+ fontSize: 30,
+ fontWeight: "bold",
+ textAlign: "center",
+}
+const TEXT: TextStyle = { fontSize: 22, textAlign: "center" }
+const IMAGE: ImageStyle = {
+ width: "100%",
+ height: 420,
+ resizeMode: "contain",
+ marginVertical: fontScale > 1.1 ? spacing[4] : spacing[5],
+}
+
+export function LiveAnnouncementScreen({ navigation }: LiveAnnouncementStackProps) {
+ const LIVE_IMAGE =
+ userLocale === "he"
+ ? require("../../../assets/live-ride/live-ride-intro.png")
+ : require("../../../assets/live-ride/live-ride-intro-english.png")
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/screens/live-announcement/live-announcement-screen.ios.tsx b/app/screens/live-announcement/live-announcement-screen.ios.tsx
new file mode 100644
index 00000000..d5f89df5
--- /dev/null
+++ b/app/screens/live-announcement/live-announcement-screen.ios.tsx
@@ -0,0 +1,83 @@
+import { Image, ImageStyle, ScrollView, TextStyle, View } from "react-native"
+import { Button, Screen, Text } from "../../components"
+import { LiveAnnouncementBackground } from "./live-announcement-bg"
+import { color, fontScale, spacing } from "../../theme"
+import { translate, userLocale } from "../../i18n"
+import { LiveAnnouncementStackProps } from "../../navigators/live-activity-announcement/live-activity-announcement-stack"
+import { useSafeAreaInsets } from "react-native-safe-area-context"
+import { NextButton } from "./announcement-next-button"
+
+const SUB_TITLE: TextStyle = {
+ color: color.whiteText,
+ fontSize: 20,
+ textAlign: "center",
+ marginBottom: -4,
+ fontWeight: "400",
+}
+
+const TITLE: TextStyle = {
+ color: color.whiteText,
+ fontSize: 42,
+ textAlign: "center",
+ marginBottom: spacing[2],
+ fontWeight: "800",
+ letterSpacing: -0.8,
+}
+
+const TEXT: TextStyle = {
+ fontSize: 18,
+ textAlign: "center",
+ color: color.whiteText,
+}
+
+const LIVE_ACTIVITY_IMAGE: ImageStyle = {
+ width: "100%",
+ height: 155,
+ resizeMode: "contain",
+ marginVertical: fontScale > 1.1 ? spacing[4] : spacing[6],
+}
+
+export function LiveAnnouncementScreen({ navigation }: LiveAnnouncementStackProps) {
+ const insets = useSafeAreaInsets()
+
+ const LIVE_ACTIVITY =
+ userLocale === "he"
+ ? require("../../../assets/live-activity/live-activity-hebrew.png")
+ : require("../../../assets/live-activity/live-activity-english.png")
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ navigation.navigate("startRide")} />
+
+
+ )
+}
diff --git a/app/screens/live-announcement/live-announcement-screen.tsx b/app/screens/live-announcement/live-announcement-screen.tsx
index d5f89df5..b5949049 100644
--- a/app/screens/live-announcement/live-announcement-screen.tsx
+++ b/app/screens/live-announcement/live-announcement-screen.tsx
@@ -1,83 +1,8 @@
-import { Image, ImageStyle, ScrollView, TextStyle, View } from "react-native"
-import { Button, Screen, Text } from "../../components"
-import { LiveAnnouncementBackground } from "./live-announcement-bg"
-import { color, fontScale, spacing } from "../../theme"
-import { translate, userLocale } from "../../i18n"
-import { LiveAnnouncementStackProps } from "../../navigators/live-activity-announcement/live-activity-announcement-stack"
-import { useSafeAreaInsets } from "react-native-safe-area-context"
-import { NextButton } from "./announcement-next-button"
-
-const SUB_TITLE: TextStyle = {
- color: color.whiteText,
- fontSize: 20,
- textAlign: "center",
- marginBottom: -4,
- fontWeight: "400",
-}
-
-const TITLE: TextStyle = {
- color: color.whiteText,
- fontSize: 42,
- textAlign: "center",
- marginBottom: spacing[2],
- fontWeight: "800",
- letterSpacing: -0.8,
-}
-
-const TEXT: TextStyle = {
- fontSize: 18,
- textAlign: "center",
- color: color.whiteText,
-}
-
-const LIVE_ACTIVITY_IMAGE: ImageStyle = {
- width: "100%",
- height: 155,
- resizeMode: "contain",
- marginVertical: fontScale > 1.1 ? spacing[4] : spacing[6],
-}
-
-export function LiveAnnouncementScreen({ navigation }: LiveAnnouncementStackProps) {
- const insets = useSafeAreaInsets()
-
- const LIVE_ACTIVITY =
- userLocale === "he"
- ? require("../../../assets/live-activity/live-activity-hebrew.png")
- : require("../../../assets/live-activity/live-activity-english.png")
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- navigation.navigate("startRide")} />
-
-
- )
-}
+import { Platform } from "react-native"
+import { LiveAnnouncementScreen as LiveAnnouncementScreenAndroid } from "./live-announcement-screen.android"
+import { LiveAnnouncementScreen as LiveAnnouncementScreenIOS } from "./live-announcement-screen.ios"
+
+export const LiveAnnouncementScreen = Platform.select({
+ ios: LiveAnnouncementScreenIOS,
+ android: LiveAnnouncementScreenAndroid,
+})
diff --git a/app/screens/live-announcement/start-ride-announcement-screen.android.tsx b/app/screens/live-announcement/start-ride-announcement-screen.android.tsx
new file mode 100644
index 00000000..7dbc409b
--- /dev/null
+++ b/app/screens/live-announcement/start-ride-announcement-screen.android.tsx
@@ -0,0 +1,49 @@
+import { ImageStyle, ScrollView, TextStyle, View } from "react-native"
+import Video from "react-native-video"
+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",
+}
+
+const TEXT: TextStyle = { fontSize: 22, textAlign: "center" }
+
+const VIDEO_STYLE: ImageStyle = {
+ width: "100%",
+ aspectRatio: 1,
+ marginVertical: spacing[4],
+ borderRadius: 8,
+}
+
+const START_RIDE_VIDEO_HEBREW = require("../../../assets/live-ride/start-live-ride.mp4")
+const START_RIDE_VIDEO_ENGLISH = require("../../../assets/live-ride/start-live-ride-english.mp4")
+
+export function StartRideAnnouncement({ navigation }: LiveAnnouncementStackProps) {
+ const START_RIDE_VIDEO = userLocale === "he" ? START_RIDE_VIDEO_HEBREW : START_RIDE_VIDEO_ENGLISH
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/screens/live-announcement/start-ride-announcement-screen.tsx b/app/screens/live-announcement/start-ride-announcement-screen.ios.tsx
similarity index 100%
rename from app/screens/live-announcement/start-ride-announcement-screen.tsx
rename to app/screens/live-announcement/start-ride-announcement-screen.ios.tsx
diff --git a/app/screens/live-announcement/start-ride-screen.tsx b/app/screens/live-announcement/start-ride-screen.tsx
new file mode 100644
index 00000000..18790bd7
--- /dev/null
+++ b/app/screens/live-announcement/start-ride-screen.tsx
@@ -0,0 +1,8 @@
+import { Platform } from "react-native"
+import { StartRideAnnouncement as StartRideAnnouncementAndroid } from "./start-ride-announcement-screen.android"
+import { StartRideAnnouncement as StartRideAnnouncementIOS } from "./start-ride-announcement-screen.ios"
+
+export const StartRideAnnouncement = Platform.select({
+ ios: StartRideAnnouncementIOS,
+ android: StartRideAnnouncementAndroid,
+})
diff --git a/app/screens/live-announcement/support-us-announcement-screen.android.tsx b/app/screens/live-announcement/support-us-announcement-screen.android.tsx
new file mode 100644
index 00000000..9f9b9eb2
--- /dev/null
+++ b/app/screens/live-announcement/support-us-announcement-screen.android.tsx
@@ -0,0 +1,123 @@
+import { Dimensions, Image, ImageStyle, ScrollView, TextStyle, View, ViewStyle } from "react-native"
+import { Button, Screen, Text } from "../../components"
+import { color, fontScale, spacing } from "../../theme"
+import { translate } from "../../i18n"
+import { LiveAnnouncementStackProps } from "../../navigators/live-activity-announcement/live-activity-announcement-stack"
+import { useSafeAreaInsets } from "react-native-safe-area-context"
+import * as storage from "../../utils/storage"
+import { useIsDarkMode } from "../../hooks"
+import { NextButton } from "./announcement-next-button"
+import analytics from "@react-native-firebase/analytics"
+
+const deviceHeight = Dimensions.get("screen").height
+const isHighDevice = deviceHeight > 800
+
+const TITLE: TextStyle = {
+ fontSize: 30,
+ textAlign: "center",
+ marginBottom: spacing[2],
+ fontWeight: "800",
+ letterSpacing: -0.8,
+}
+
+const TEXT: TextStyle = {
+ fontSize: isHighDevice ? 20 : 18,
+ textAlign: "center",
+}
+
+const AVATARS: ViewStyle = {
+ marginTop: spacing[2],
+ marginBottom: spacing[5],
+ flexDirection: "row",
+ gap: -16,
+ alignItems: "center",
+ justifyContent: "center",
+}
+
+const AVATAR_WRAPPER: ViewStyle = {
+ elevation: 4,
+ borderRadius: 100,
+}
+
+const AVATAR: ImageStyle = {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ resizeMode: "cover",
+}
+
+const GUY_IMAGE = require("../../../assets/live-activity/guy.jpeg")
+const MATAN_IMAGE = require("../../../assets/live-activity/matan.jpeg")
+
+export function SupportUsScreen({ navigation }: LiveAnnouncementStackProps) {
+ const isDarkMode = useIsDarkMode()
+ const insets = useSafeAreaInsets()
+
+ const finish = () => {
+ navigation.navigate("planner")
+ storage.save("seenLiveAnnouncement", new Date().toISOString())
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/screens/live-announcement/support-us-announcement-screen.ios.tsx b/app/screens/live-announcement/support-us-announcement-screen.ios.tsx
new file mode 100644
index 00000000..3febae6d
--- /dev/null
+++ b/app/screens/live-announcement/support-us-announcement-screen.ios.tsx
@@ -0,0 +1,132 @@
+import { useLayoutEffect } from "react"
+import { Dimensions, Image, ImageStyle, ScrollView, TextStyle, View, ViewStyle } from "react-native"
+import { Button, Screen, Text } from "../../components"
+import { LiveAnnouncementBackground } from "./live-announcement-bg"
+import { color, fontScale, spacing } from "../../theme"
+import { translate } from "../../i18n"
+import { LiveAnnouncementStackProps } from "../../navigators/live-activity-announcement/live-activity-announcement-stack"
+import { useSafeAreaInsets } from "react-native-safe-area-context"
+import * as storage from "../../utils/storage"
+import { useIsDarkMode } from "../../hooks"
+import { NextButton } from "./announcement-next-button"
+import analytics from "@react-native-firebase/analytics"
+
+const deviceHeight = Dimensions.get("screen").height
+const isHighDevice = deviceHeight > 800
+
+const TITLE: TextStyle = {
+ color: color.whiteText,
+ fontSize: 30,
+ textAlign: "center",
+ marginBottom: spacing[2],
+ fontWeight: "800",
+ letterSpacing: -0.8,
+}
+
+const TEXT: TextStyle = {
+ fontSize: 18,
+ textAlign: "center",
+ color: color.whiteText,
+}
+
+const AVATARS: ViewStyle = {
+ marginTop: spacing[2],
+ marginBottom: spacing[5],
+ flexDirection: "row",
+ gap: -24,
+ alignItems: "center",
+ justifyContent: "center",
+}
+
+const AVATAR_WRAPPER = {
+ shadowColor: "#333",
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.7,
+ shadowRadius: 5,
+}
+
+const AVATAR: ImageStyle = {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ resizeMode: "cover",
+}
+
+const GUY_IMAGE = require("../../../assets/live-activity/guy.jpeg")
+const MATAN_IMAGE = require("../../../assets/live-activity/matan.jpeg")
+
+export function SupportUsScreen({ navigation }: LiveAnnouncementStackProps) {
+ const isDarkMode = useIsDarkMode()
+ const insets = useSafeAreaInsets()
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ headerLeft: null,
+ })
+ }, [])
+
+ const finish = () => {
+ navigation.navigate("planner")
+ storage.save("seenLiveAnnouncement", new Date().toISOString())
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ analytics().logEvent("live_announcement_tip_jar_press")
+ finish()
+
+ setTimeout(() => {
+ navigation.navigate("settingsStack", { screen: "tipJar" })
+ }, 150)
+ }}
+ />
+
+ {
+ analytics().logEvent("live_announcement_done_press")
+ finish()
+ }}
+ />
+
+
+
+ )
+}
diff --git a/app/screens/live-announcement/support-us-announcement-screen.tsx b/app/screens/live-announcement/support-us-announcement-screen.tsx
index 3febae6d..d11cdb92 100644
--- a/app/screens/live-announcement/support-us-announcement-screen.tsx
+++ b/app/screens/live-announcement/support-us-announcement-screen.tsx
@@ -1,132 +1,8 @@
-import { useLayoutEffect } from "react"
-import { Dimensions, Image, ImageStyle, ScrollView, TextStyle, View, ViewStyle } from "react-native"
-import { Button, Screen, Text } from "../../components"
-import { LiveAnnouncementBackground } from "./live-announcement-bg"
-import { color, fontScale, spacing } from "../../theme"
-import { translate } from "../../i18n"
-import { LiveAnnouncementStackProps } from "../../navigators/live-activity-announcement/live-activity-announcement-stack"
-import { useSafeAreaInsets } from "react-native-safe-area-context"
-import * as storage from "../../utils/storage"
-import { useIsDarkMode } from "../../hooks"
-import { NextButton } from "./announcement-next-button"
-import analytics from "@react-native-firebase/analytics"
-
-const deviceHeight = Dimensions.get("screen").height
-const isHighDevice = deviceHeight > 800
-
-const TITLE: TextStyle = {
- color: color.whiteText,
- fontSize: 30,
- textAlign: "center",
- marginBottom: spacing[2],
- fontWeight: "800",
- letterSpacing: -0.8,
-}
-
-const TEXT: TextStyle = {
- fontSize: 18,
- textAlign: "center",
- color: color.whiteText,
-}
-
-const AVATARS: ViewStyle = {
- marginTop: spacing[2],
- marginBottom: spacing[5],
- flexDirection: "row",
- gap: -24,
- alignItems: "center",
- justifyContent: "center",
-}
-
-const AVATAR_WRAPPER = {
- shadowColor: "#333",
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 0.7,
- shadowRadius: 5,
-}
-
-const AVATAR: ImageStyle = {
- width: 120,
- height: 120,
- borderRadius: 60,
- resizeMode: "cover",
-}
-
-const GUY_IMAGE = require("../../../assets/live-activity/guy.jpeg")
-const MATAN_IMAGE = require("../../../assets/live-activity/matan.jpeg")
-
-export function SupportUsScreen({ navigation }: LiveAnnouncementStackProps) {
- const isDarkMode = useIsDarkMode()
- const insets = useSafeAreaInsets()
-
- useLayoutEffect(() => {
- navigation.setOptions({
- headerLeft: null,
- })
- }, [])
-
- const finish = () => {
- navigation.navigate("planner")
- storage.save("seenLiveAnnouncement", new Date().toISOString())
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- analytics().logEvent("live_announcement_tip_jar_press")
- finish()
-
- setTimeout(() => {
- navigation.navigate("settingsStack", { screen: "tipJar" })
- }, 150)
- }}
- />
-
- {
- analytics().logEvent("live_announcement_done_press")
- finish()
- }}
- />
-
-
-
- )
-}
+import { Platform } from "react-native"
+import { SupportUsScreen as SupportUsScreenIOS } from "./support-us-announcement-screen.ios"
+import { SupportUsScreen as SupportUsScreenAndroid } from "./support-us-announcement-screen.android"
+
+export const SupportUsScreen = Platform.select({
+ ios: SupportUsScreenIOS,
+ android: SupportUsScreenAndroid,
+})
diff --git a/app/screens/planner/planner-screen-header.tsx b/app/screens/planner/planner-screen-header.tsx
index 9ac14aeb..9c975f1e 100644
--- a/app/screens/planner/planner-screen-header.tsx
+++ b/app/screens/planner/planner-screen-header.tsx
@@ -45,9 +45,9 @@ export const PlannerScreenHeader = observer(function PlannerScreenHeader() {
const [displayNewBadge, setDisplayNewBadge] = useState(false)
useEffect(() => {
- // display the "new" badge on ios devices if the user has stations selected (it's not the
- // initial launch) and they haven't seen the live announcement screen yet
- if (routePlan.origin && routePlan.destination && canRunLiveActivities) {
+ // display the "new" badge if the user has stations selected (not the initial launch)
+ // and they haven't seen the live announcement screen yet
+ if (routePlan.origin && routePlan.destination) {
storage.load("seenLiveAnnouncement").then((hasSeenLiveAnnouncementScreen) => {
if (!hasSeenLiveAnnouncementScreen) setDisplayNewBadge(true)
})
@@ -70,7 +70,7 @@ export const PlannerScreenHeader = observer(function PlannerScreenHeader() {
analytics().logEvent("open_live_ride_modal_pressed")
}}
>
-
+ {Platform.OS === "ios" && }
)}
diff --git a/app/screens/planner/planner-screen.tsx b/app/screens/planner/planner-screen.tsx
index 84028840..700e6ce7 100644
--- a/app/screens/planner/planner-screen.tsx
+++ b/app/screens/planner/planner-screen.tsx
@@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect } from "react"
import { observer } from "mobx-react-lite"
-import { View, Animated, ViewStyle, TextStyle, Dimensions, AppState, AppStateStatus, Platform } from "react-native"
+import { View, Animated, ViewStyle, TextStyle, Dimensions, AppState, AppStateStatus, Platform, Alert } from "react-native"
import { Screen, Button, Text, StationCard, DummyInput, ChangeDirectionButton, ResetTimeButton } from "../../components"
import { useStores } from "../../models"
import HapticFeedback from "react-native-haptic-feedback"
@@ -238,6 +238,11 @@ export const PlannerScreen = observer(function PlannerScreen({ navigation }: Pla
title={translate("plan.find")}
onPress={onGetRoutePress}
disabled={!routePlan.origin || !routePlan.destination || routePlan.origin.id === routePlan.destination.id}
+ onDisabledPress={() => {
+ if (routePlan.origin.id === routePlan.destination.id) {
+ Alert.alert(translate("routes.sameStationsMessage"))
+ }
+ }}
/>
diff --git a/app/screens/route-details/components/start-ride-button.tsx b/app/screens/route-details/components/start-ride-button.tsx
index edafa6b6..193480e6 100644
--- a/app/screens/route-details/components/start-ride-button.tsx
+++ b/app/screens/route-details/components/start-ride-button.tsx
@@ -1,4 +1,4 @@
-import { Alert, Dimensions, Image, ImageStyle, Linking, Platform, View, ViewStyle } from "react-native"
+import { Alert, Dimensions, Image, ImageStyle, Linking, PermissionsAndroid, Platform, View, ViewStyle } from "react-native"
import { observer } from "mobx-react-lite"
import * as storage from "../../../utils/storage"
import { useSafeAreaInsets } from "react-native-safe-area-context"
@@ -6,11 +6,11 @@ import HapticFeedback from "react-native-haptic-feedback"
import analytics from "@react-native-firebase/analytics"
import crashlytics from "@react-native-firebase/crashlytics"
import { Button } from "../../../components"
-import { isRTL, translate } from "../../../i18n"
+import { isRTL, translate, userLocale } from "../../../i18n"
import { RouteItem } from "../../../services/api"
import { differenceInMinutes, isAfter } from "date-fns"
import { timezoneCorrection } from "../../../utils/helpers/date-helpers"
-import { color } from "../../../theme"
+import { color, fontScale } from "../../../theme"
import { useStores } from "../../../models"
import { canRunLiveActivities } from "../../../utils/ios-helpers"
@@ -62,9 +62,14 @@ export const StartRideButton = observer(function StartRideButton(props: StartRid
const isRouteInPast = isAfter(timezoneCorrection(new Date()).getTime(), route.arrivalTime)
const isRouteInFuture = differenceInMinutes(route.departureTime, timezoneCorrection(new Date()).getTime()) > 60
- const activeRide = !!ride.id
- const areActivitiesDisabled = !canRunLiveActivities || !(ride?.activityAuthorizationInfo?.areActivitiesEnabled ?? true)
- const isStartRideButtonDisabled = isRouteInFuture || isRouteInPast || areActivitiesDisabled || activeRide
+ const areActivitiesDisabled = Platform.select({
+ ios: () => !canRunLiveActivities || !(ride?.activityAuthorizationInfo?.areActivitiesEnabled ?? true),
+ android: () => {
+ const result = PermissionsAndroid.RESULTS[PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS]
+ return result && result !== "granted"
+ },
+ })
+ const isStartRideButtonDisabled = isRouteInFuture || isRouteInPast || areActivitiesDisabled()
const shouldDisplayFirstRideAlert = async () => {
const isFirstRideAlertEnabled = isAfterTargetDate
@@ -80,6 +85,46 @@ export const StartRideButton = observer(function StartRideButton(props: StartRid
return false
}
+ const startRide = async () => {
+ if (ride.id) {
+ return Alert.alert(translate("ride.rideExistsTitle"), translate("ride.rideExistsMessage"), [
+ {
+ style: "cancel",
+ text: translate("common.cancel"),
+ },
+ {
+ text: translate("ride.startNewRide"),
+ onPress: async () => {
+ await ride.stopRide(ride.id)
+ return startRide()
+ },
+ },
+ ])
+ }
+
+ crashlytics().log("Start ride button pressed")
+ crashlytics().setAttributes({
+ route: JSON.stringify(route),
+ rideId: ride.id ?? "null",
+ })
+
+ if (Platform.OS === "ios") {
+ shouldDisplayFirstRideAlert().then((isFirstRide) => {
+ if (isFirstRide) {
+ props.openFirstRideAlertSheet()
+ analytics().logEvent("first_live_ride_alert")
+ }
+ })
+ } else if (Number(Platform.Version) >= 33) {
+ const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS)
+ if (result !== "granted") return
+ }
+
+ HapticFeedback.trigger("notificationSuccess")
+ ride.startRide(route)
+ analytics().logEvent("start_live_ride")
+ }
+
return (
}
@@ -98,15 +146,19 @@ export const StartRideButton = observer(function StartRideButton(props: StartRid
title={translate("ride.startRide")}
loading={ride.loading}
disabled={isStartRideButtonDisabled}
+ onPress={startRide}
onDisabledPress={() => {
HapticFeedback.trigger("notificationError")
let disabledReason = ""
- if (activeRide) {
- disabledReason = "Active ride already exists"
- Alert.alert(translate("ride.rideExistsTitle"), translate("ride.rideExistsMessage"))
- } else if (areActivitiesDisabled) {
- disabledReason = "Live Activities disabled"
- Alert.alert(translate("ride.disabledTitle"), translate("ride.disabledMessage"), [
+ if (areActivitiesDisabled()) {
+ disabledReason = Platform.OS === "ios" ? "Live Activities disabled" : "Notifications disbled"
+ const alertTitle =
+ Platform.OS === "ios" ? translate("ride.liveActivitiesDisabledTitle") : translate("ride.notificationsDisabledTitle")
+ const alertMessage =
+ Platform.OS === "ios"
+ ? translate("ride.liveActivitiesDisabledMessage")
+ : translate("ride.notificationsDisabledMessage")
+ Alert.alert(alertTitle, alertMessage, [
{
style: "cancel",
text: translate("common.cancel"),
@@ -133,26 +185,6 @@ export const StartRideButton = observer(function StartRideButton(props: StartRid
reason: disabledReason,
})
}}
- onPress={() => {
- crashlytics().log("Start ride button pressed")
- crashlytics().setAttributes({
- route: JSON.stringify(route),
- rideId: ride.id ?? "null",
- })
-
- if (Platform.OS === "ios") {
- shouldDisplayFirstRideAlert().then((isFirstRide) => {
- if (isFirstRide) {
- props.openFirstRideAlertSheet()
- analytics().logEvent("first_live_ride_alert")
- }
- })
- }
-
- HapticFeedback.trigger("notificationSuccess")
- ride.startRide(route)
- analytics().logEvent("start_live_ride")
- }}
/>
)
diff --git a/app/screens/route-details/route-details-screen.tsx b/app/screens/route-details/route-details-screen.tsx
index f7dbdb4a..94023383 100644
--- a/app/screens/route-details/route-details-screen.tsx
+++ b/app/screens/route-details/route-details-screen.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react-native/no-inline-styles */
import React, { useEffect, useMemo, useRef, useState } from "react"
-import { View, ViewStyle } from "react-native"
+import { Platform, View, ViewStyle } from "react-native"
import { observer } from "mobx-react-lite"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { SharedElement } from "react-navigation-shared-element"
@@ -26,8 +26,6 @@ import {
import BottomSheet from "@gorhom/bottom-sheet"
import { FirstRideAlert } from "./components/first-ride-alert"
import { canRunLiveActivities } from "../../utils/ios-helpers"
-import { CreateOptions } from "react-native-add-calendar-event"
-import { translate } from "../../i18n"
const ROOT: ViewStyle = {
flex: 1,
@@ -63,6 +61,12 @@ export const RouteDetailsScreen = observer(function RouteDetailsScreen({ route }
setShouldFadeRideButton(true)
}, [])
+ useEffect(() => {
+ if (ride.id && progress.status === "arrived") {
+ ride.stopRide(ride.id)
+ }
+ }, [progress.status, ride.id])
+
return (
+
+
{isRideOnThisRoute && (
-
+
)}
- {/** TODO: Remove iOS Check */}
- {canRunLiveActivities && !isRideOnThisRoute && (
-
+ {(Platform.OS === "android" || canRunLiveActivities) && !isRideOnThisRoute && (
+
)}
+
)
diff --git a/app/screens/route-list/components/no-trains-found-msg.tsx b/app/screens/route-list/components/no-trains-found-msg.tsx
index d5b3067d..878fc201 100644
--- a/app/screens/route-list/components/no-trains-found-msg.tsx
+++ b/app/screens/route-list/components/no-trains-found-msg.tsx
@@ -42,6 +42,8 @@ export const NoTrainsFoundMessage = observer(function NoTrainsFoundMessage() {
const originId = routePlan.origin.id
const destinationId = routePlan.destination.id
+ const shouldShowAnnouncements = originId !== destinationId
+
const filterRelatedAnnouncements = (a: Announcement) => {
// Filter related updates to the route
// if the announcement stations length equals 0, it means that the update is relevant to all stations
@@ -57,27 +59,33 @@ export const NoTrainsFoundMessage = observer(function NoTrainsFoundMessage() {
setRelatedAnnouncements(related)
}
- findRelatedAnnouncements()
- }, [])
+ if (shouldShowAnnouncements) {
+ findRelatedAnnouncements()
+ }
+ }, [shouldShowAnnouncements])
return (
-
-
-
-
-
-
-
-
- {relatedAnnouncements.length > 0 ? (
- relatedAnnouncements.map((a, index) => )
- ) : (
-
+
+
+ {shouldShowAnnouncements && (
+ <>
+
+
+
+
+
+
+ {relatedAnnouncements.length > 0 ? (
+ relatedAnnouncements.map((a, index) => )
+ ) : (
+
+ )}
+ >
)}
)
diff --git a/app/screens/select-station/select-station-screen.tsx b/app/screens/select-station/select-station-screen.tsx
index c67619f0..760acb1d 100644
--- a/app/screens/select-station/select-station-screen.tsx
+++ b/app/screens/select-station/select-station-screen.tsx
@@ -75,7 +75,7 @@ export const SelectStationScreen = observer(function SelectStationScreen({ navig
throw new Error("Selection type was not provided.")
}
recentSearches.save({ id: station.id })
- navigation.navigate("planner")
+ navigation.goBack()
}}
/>
),
diff --git a/app/services/api/index.ts b/app/services/api/index.ts
index a12bb550..e7f5984c 100644
--- a/app/services/api/index.ts
+++ b/app/services/api/index.ts
@@ -1,2 +1,3 @@
-export * from "./api"
-export * from "./api.types"
+export * from "./rail-api"
+export * from "./rail-api.types"
+export * from "./ride-api"
diff --git a/app/services/api/api.ts b/app/services/api/rail-api.ts
similarity index 94%
rename from app/services/api/api.ts
rename to app/services/api/rail-api.ts
index c86b1508..6d03329a 100644
--- a/app/services/api/api.ts
+++ b/app/services/api/rail-api.ts
@@ -1,6 +1,6 @@
import axios, { AxiosInstance, AxiosResponse } from "axios"
import { LanguageCode } from "../../i18n"
-import { AnnouncementApiResult } from "./api.types"
+import { AnnouncementApiResult } from "./rail-api.types"
export class RailApi {
axiosInstance: AxiosInstance
diff --git a/app/services/api/api.types.ts b/app/services/api/rail-api.types.ts
similarity index 100%
rename from app/services/api/api.types.ts
rename to app/services/api/rail-api.types.ts
diff --git a/app/services/api/ride-api.ts b/app/services/api/ride-api.ts
new file mode 100644
index 00000000..0f655512
--- /dev/null
+++ b/app/services/api/ride-api.ts
@@ -0,0 +1,67 @@
+import axios, { AxiosInstance } from "axios"
+import { RouteItem } from "./rail-api.types"
+import { userLocale } from "../../i18n"
+import { head, last } from "lodash"
+
+export class RideApi {
+ axiosInstance: AxiosInstance
+
+ constructor() {
+ const env: string = "production"
+ const envPath = env === "production" ? "" : "-" + env
+ const baseURL = `https://better-rail${envPath}.up.railway.app/api/v1`
+
+ this.axiosInstance = axios.create({
+ baseURL,
+ timeout: 30000,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ }
+
+ async startRide(route: RouteItem, token: string): Promise {
+ try {
+ const response = await this.axiosInstance.post("/ride", {
+ token,
+ provider: "android",
+ locale: userLocale,
+ departureDate: route.departureTimeString,
+ originId: head(route.trains).originStationId,
+ destinationId: last(route.trains).destinationStationId,
+ trains: route.trains.map((train) => train.trainNumber),
+ })
+
+ return response.data?.success ? response.data?.rideId : null
+ } catch {
+ return null
+ }
+ }
+
+ async endRide(rideId: string): Promise {
+ try {
+ const response = await this.axiosInstance.delete("/ride", {
+ data: {
+ rideId,
+ },
+ })
+
+ return response.data?.success
+ } catch {
+ return false
+ }
+ }
+
+ async updateRideToken(rideId: string, token: string): Promise {
+ try {
+ const response = await this.axiosInstance.patch("/ride/updateToken", {
+ rideId,
+ token,
+ })
+
+ return response.data?.success
+ } catch {
+ return false
+ }
+ }
+}
diff --git a/app/services/api/route-api.ts b/app/services/api/route-api.ts
index 345b4472..dacb0293 100644
--- a/app/services/api/route-api.ts
+++ b/app/services/api/route-api.ts
@@ -1,10 +1,11 @@
-import { railApi } from "./api"
+import { railApi } from "./rail-api"
import { AxiosResponse } from "axios"
import { stationsObject, stationLocale } from "../../data/stations"
-import { RailApiGetRoutesResult } from "./api.types"
+import { RailApiGetRoutesResult } from "./rail-api.types"
import { formatRouteDuration, isOneHourDifference, routeDurationInMs } from "../../utils/helpers/date-helpers"
import { RouteItem } from "."
import { getHours, parse, isSameDay, addDays } from "date-fns"
+import { findClosestStationInRoute, getTrainFromStationId } from "../../utils/helpers/ride-helpers"
export class RouteApi {
private api = railApi
@@ -96,9 +97,13 @@ export class RouteApi {
})
const routesWithWarning = formattedRoutes.map((route) => {
+ const nextStationId = findClosestStationInRoute(route as RouteItem)
+ const train = getTrainFromStationId(route as RouteItem, nextStationId)
+
+ const delay = train.delay
const isMuchLonger = isRouteIsMuchLongerThanOtherRoutes(route as RouteItem, formattedRoutes as RouteItem[])
const isMuchShorter = isRouteMuchShorterThanOtherRoutes(route as RouteItem, formattedRoutes as RouteItem[])
- return Object.assign({}, route, { isMuchShorter, isMuchLonger })
+ return Object.assign({}, route, { isMuchShorter, isMuchLonger, delay })
})
return routesWithWarning
diff --git a/app/utils/android-helpers.ts b/app/utils/android-helpers.ts
new file mode 100644
index 00000000..60bc1846
--- /dev/null
+++ b/app/utils/android-helpers.ts
@@ -0,0 +1,181 @@
+import messaging, { FirebaseMessagingTypes } from "@react-native-firebase/messaging"
+import notifee, { AndroidImportance, AndroidLaunchActivityFlag } from "@notifee/react-native"
+import { RideState, RideStatus, getStatusEndDate, rideProgress } from "../hooks/use-ride-progress"
+import { RideApi, RouteItem } from "../services/api"
+import { findClosestStationInRoute, getRideStatus, getTrainFromStationId } from "./helpers/ride-helpers"
+import { differenceInMinutes, format } from "date-fns"
+import Preferences from "react-native-default-preference"
+import { getInitialLanguage, translate } from "../i18n"
+import i18n from "i18n-js"
+
+const rideApi = new RideApi()
+let unsubscribeTokenUpdates: () => void
+
+const getRideNotificationId = () => Preferences.get("rideNotificationId")
+const setRideRoute = (route: RouteItem) => Preferences.set("rideRoute", JSON.stringify(route))
+const setRideNotificationId = (notificationId: string) => Preferences.set("rideNotificationId", notificationId)
+const getRideRoute = async () => {
+ const savedRoute = await Preferences.get("rideRoute")
+ return savedRoute && (JSON.parse(savedRoute) as RouteItem)
+}
+
+export const configureAndroidNotifications = async () => {
+ notifee.createChannel({
+ id: "better-rail",
+ name: "Better Rail",
+ description: "Get live ride notifications",
+ importance: AndroidImportance.HIGH,
+ sound: "default",
+ })
+
+ notifee.createChannel({
+ id: "better-rail-live",
+ name: "Better Rail Live",
+ description: "Get live ride persistent notification",
+ vibration: false,
+ })
+
+ const onRecievedMessage = async (message: FirebaseMessagingTypes.RemoteMessage) => {
+ if (message.data?.type !== "live-ride") return
+
+ if (message.data.notifee) {
+ notifee.displayNotification({
+ ...JSON.parse(message.data.notifee),
+ android: {
+ channelId: "better-rail",
+ smallIcon: "notification_icon",
+ timeoutAfter: 60 * 1000,
+ pressAction: {
+ id: "default",
+ launchActivity: "com.betterrail.MainActivity",
+ launchActivityFlags: [AndroidLaunchActivityFlag.SINGLE_TOP],
+ },
+ },
+ })
+ }
+
+ const state: RideState = {
+ status: message.data.status as RideStatus,
+ delay: Number(message.data.delay),
+ nextStationId: Number(message.data.nextStationId),
+ }
+
+ const rideNotificationId = await getRideNotificationId()
+ if (rideNotificationId && state) {
+ const rideRoute = await getRideRoute()
+ updateNotification(rideRoute, state)
+ }
+ }
+
+ messaging().onMessage(onRecievedMessage)
+ messaging().setBackgroundMessageHandler(onRecievedMessage)
+ notifee.onBackgroundEvent(() => Promise.resolve())
+}
+
+export const startRideNotifications = async (route: RouteItem) => {
+ const token = await messaging().getToken()
+ const rideId = await rideApi.startRide(route, token)
+
+ if (!rideId) {
+ throw new Error("Couldn't start ride")
+ }
+
+ unsubscribeTokenUpdates = messaging().onTokenRefresh((newToken) => {
+ rideApi.updateRideToken(rideId, newToken)
+ })
+
+ await setRideRoute(route)
+ const nextStationId = findClosestStationInRoute(route)
+ const train = getTrainFromStationId(route, nextStationId)
+ const status = getRideStatus(route, train, nextStationId)
+
+ const state: RideState = {
+ status,
+ nextStationId,
+ delay: train.delay,
+ }
+
+ const rideNotificationId = await updateNotification(route, state)
+ await setRideNotificationId(rideNotificationId)
+ return rideId
+}
+
+export const cancelNotifications = async () => {
+ messaging().deleteToken()
+ if (unsubscribeTokenUpdates) unsubscribeTokenUpdates()
+
+ const rideNotificationId = await getRideNotificationId()
+ if (rideNotificationId) {
+ notifee.cancelNotification(rideNotificationId)
+ Preferences.clearMultiple(["rideRoute", "rideNotificationId"])
+ }
+}
+
+export const endRideNotifications = async (rideId: string) => {
+ await cancelNotifications()
+ return rideApi.endRide(rideId)
+}
+
+const updateNotification = async (route: RouteItem, state: RideState) => {
+ const rideNotificationId = await getRideNotificationId()
+ const userLanguage = (await Preferences.get("userLocale")) || getInitialLanguage()
+ i18n.locale = userLanguage
+
+ return notifee.displayNotification({
+ [rideNotificationId && "id"]: rideNotificationId,
+ title: getTitleText(route, state),
+ body: getBodyText(route, state),
+ android: {
+ channelId: "better-rail-live",
+ smallIcon: "notification_icon",
+ ongoing: state.status !== "arrived",
+ autoCancel: state.status === "arrived",
+ timeoutAfter: state.status === "arrived" ? 3 * 60 * 1000 : undefined,
+ pressAction: {
+ id: "default",
+ launchActivity: "com.betterrail.MainActivity",
+ launchActivityFlags: [AndroidLaunchActivityFlag.SINGLE_TOP],
+ },
+ },
+ })
+}
+
+const getTitleText = (route: RouteItem, state: RideState) => {
+ const targetDate = getStatusEndDate(route, state)
+ const minutes = differenceInMinutes(targetDate, Date.now(), { roundingMethod: "ceil" })
+ const time = format(targetDate, "HH:mm")
+ const timeText = "(" + time + ")"
+
+ if (state.status === "waitForTrain" || state.status === "inExchange") {
+ if (minutes < 2) return translate("ride.departsNow") + " " + timeText
+ else return translate("ride.departsIn", { minutes }) + " " + timeText
+ } else if (state.status === "inTransit") {
+ return translate("ride.arrivingIn", { minutes }) + " " + timeText
+ } else {
+ return translate("ride.arrived")
+ }
+}
+
+const getBodyText = (route: RouteItem, state: RideState) => {
+ if (state.status === "waitForTrain" || state.status === "inExchange") {
+ const train = getTrainFromStationId(route, state.nextStationId)
+ return translate("ride.trainInfo", {
+ trainNumber: train.trainNumber,
+ lastStop: train.lastStop,
+ platform: train.originPlatform,
+ })
+ } else if (state.status === "inTransit") {
+ const progress = rideProgress(route, state.nextStationId)
+ const stopsLeft = progress[1] - progress[0]
+ if (stopsLeft === 1) return translate("ride.getOffNextStop")
+ else return translate("ride.getOffInStops", { stopsLeft })
+ } else {
+ return translate("ride.greeting")
+ }
+}
+
+export default {
+ startRideNotifications,
+ endRideNotifications,
+ cancelNotifications,
+}
diff --git a/app/utils/helpers/ride-helpers.ts b/app/utils/helpers/ride-helpers.ts
index 9c94c64c..d1f3fb85 100644
--- a/app/utils/helpers/ride-helpers.ts
+++ b/app/utils/helpers/ride-helpers.ts
@@ -2,6 +2,10 @@ import { addMinutes, differenceInSeconds } from "date-fns"
import { RideStatus } from "../../hooks/use-ride-progress"
import { RouteItem, Train } from "../../services/api"
import { isEqual, last } from "lodash"
+import { NavigationContainerRef } from "@react-navigation/native"
+import { MutableRefObject } from "react"
+import { RootStore } from "../../models"
+import { PrimaryParamList, RootParamList } from "../../navigators"
/**
* Find the closest station to the current time.
@@ -100,3 +104,33 @@ export function getRideStatus(route: RouteItem, train: Train, nextStationId: num
return "inTransit"
}
+
+export const openActiveRide = (rootStore: RootStore, navigationRef: MutableRefObject>) => {
+ const { route, originId, destinationId } = rootStore.ride
+ if (!route) return
+
+ const { name: screenName, params: screenParams } = navigationRef.current?.getCurrentRoute() ?? {}
+
+ if (screenName !== "routeDetails") {
+ // @ts-expect-error navigator type
+ navigationRef.current?.navigate("activeRideStack", {
+ screen: "activeRide",
+ params: { routeItem: route, originId: originId, destinationId: destinationId },
+ })
+ } else {
+ // if we're on the route details screen, we need to check if it's the same route
+ // as the live activity route, by using the provided train numbers in the activity deep link url
+ const activityTrainNumbers = route.trains.map((t) => t.trainNumber).join()
+
+ const { routeItem } = screenParams as PrimaryParamList["routeDetails"]
+ const routeTrainNumbers = routeItem.trains.map((t) => t.trainNumber).join()
+
+ if (!isEqual(activityTrainNumbers, routeTrainNumbers)) {
+ // @ts-expect-error navigator type
+ navigationRef.current?.navigate("activeRideStack", {
+ screen: "activeRide",
+ params: { routeItem: route, originId: originId, destinationId: destinationId },
+ })
+ }
+ }
+}
diff --git a/assets/live-ride/live-ride-intro-english.png b/assets/live-ride/live-ride-intro-english.png
new file mode 100644
index 00000000..960d5ce8
Binary files /dev/null and b/assets/live-ride/live-ride-intro-english.png differ
diff --git a/assets/live-ride/live-ride-intro.png b/assets/live-ride/live-ride-intro.png
new file mode 100644
index 00000000..00f53592
Binary files /dev/null and b/assets/live-ride/live-ride-intro.png differ
diff --git a/assets/live-ride/live-ride-notification-english.png b/assets/live-ride/live-ride-notification-english.png
new file mode 100644
index 00000000..7e030abe
Binary files /dev/null and b/assets/live-ride/live-ride-notification-english.png differ
diff --git a/assets/live-ride/live-ride-notification.png b/assets/live-ride/live-ride-notification.png
new file mode 100644
index 00000000..b2567306
Binary files /dev/null and b/assets/live-ride/live-ride-notification.png differ
diff --git a/assets/live-ride/start-live-ride-english.mp4 b/assets/live-ride/start-live-ride-english.mp4
new file mode 100644
index 00000000..dbdc5f07
Binary files /dev/null and b/assets/live-ride/start-live-ride-english.mp4 differ
diff --git a/assets/live-ride/start-live-ride.mp4 b/assets/live-ride/start-live-ride.mp4
new file mode 100644
index 00000000..e96ec82c
Binary files /dev/null and b/assets/live-ride/start-live-ride.mp4 differ
diff --git a/index.js b/index.js
index a1494c56..bdedbbaf 100644
--- a/index.js
+++ b/index.js
@@ -8,7 +8,12 @@
//
// It's easier just to leave it here.
import App from "./app/app.tsx"
-import { AppRegistry } from "react-native"
+import { configureAndroidNotifications } from "./app/utils/android-helpers.ts"
+import { AppRegistry, Platform } from "react-native"
+
+if (Platform.OS === "android") {
+ configureAndroidNotifications()
+}
AppRegistry.registerComponent("BetterRail", () => App)
export default App
diff --git a/ios/BetterRail.xcodeproj/project.pbxproj b/ios/BetterRail.xcodeproj/project.pbxproj
index fe1d24ba..18d1c527 100644
--- a/ios/BetterRail.xcodeproj/project.pbxproj
+++ b/ios/BetterRail.xcodeproj/project.pbxproj
@@ -1156,7 +1156,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = BetterRail/BetterRailDebug.entitlements;
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UE6BVYPPFX;
ENABLE_BITCODE = NO;
@@ -1196,7 +1196,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UE6BVYPPFX;
INFOPLIST_FILE = BetterRail/Info.plist;
@@ -1359,7 +1359,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=watchos*]" = UE6BVYPPFX;
@@ -1401,7 +1401,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=watchos*]" = UE6BVYPPFX;
@@ -1437,7 +1437,7 @@
CODE_SIGN_ENTITLEMENTS = "WatchBetterRail Extension/WatchBetterRail Extension.entitlements";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=watchos*]" = UE6BVYPPFX;
@@ -1478,7 +1478,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=watchos*]" = UE6BVYPPFX;
@@ -1516,7 +1516,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = BetterRailWidgetExtensionDebug.entitlements;
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UE6BVYPPFX;
@@ -1557,7 +1557,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UE6BVYPPFX;
@@ -1590,7 +1590,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = StationIntent/StationIntentDebug.entitlements;
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UE6BVYPPFX;
@@ -1629,7 +1629,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UE6BVYPPFX;
diff --git a/ios/BetterRail/Info.plist b/ios/BetterRail/Info.plist
index 9c289245..7981a4e6 100644
--- a/ios/BetterRail/Info.plist
+++ b/ios/BetterRail/Info.plist
@@ -23,7 +23,7 @@
CFBundleSignature
????
CFBundleVersion
- 7
+ 11
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/BetterRailWidget/Info.plist b/ios/BetterRailWidget/Info.plist
index fe9e55c2..fefd0ab4 100644
--- a/ios/BetterRailWidget/Info.plist
+++ b/ios/BetterRailWidget/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7
+ 11
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 80b56247..d379e9d3 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -34,6 +34,9 @@ PODS:
- Firebase/Crashlytics (10.7.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 10.7.0)
+ - Firebase/Messaging (10.7.0):
+ - Firebase/CoreOnly
+ - FirebaseMessaging (~> 10.7.0)
- FirebaseAnalytics/WithoutAdIdSupport (10.7.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
@@ -69,6 +72,15 @@ PODS:
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
+ - FirebaseMessaging (10.7.0):
+ - FirebaseCore (~> 10.0)
+ - FirebaseInstallations (~> 10.0)
+ - GoogleDataTransport (~> 9.2)
+ - GoogleUtilities/AppDelegateSwizzler (~> 7.8)
+ - GoogleUtilities/Environment (~> 7.8)
+ - GoogleUtilities/Reachability (~> 7.8)
+ - GoogleUtilities/UserDefaults (~> 7.8)
+ - nanopb (< 2.30910.0, >= 2.30908.0)
- FirebaseSessions (10.10.0):
- FirebaseCore (~> 10.5)
- FirebaseCoreExtension (~> 10.0)
@@ -502,8 +514,12 @@ PODS:
- React-Core
- RNCMaskedView (0.2.8):
- React-Core
+ - RNCPushNotificationIOS (1.11.0):
+ - React-Core
- RNDateTimePicker (6.7.3):
- React-Core
+ - RNDefaultPreference (1.4.4):
+ - React-Core
- RNDeviceInfo (10.6.0):
- React-Core
- RNFBAnalytics (17.5.0):
@@ -522,6 +538,11 @@ PODS:
- FirebaseCoreExtension (= 10.7.0)
- React-Core
- RNFBApp
+ - RNFBMessaging (17.5.0):
+ - Firebase/Messaging (= 10.7.0)
+ - FirebaseCoreExtension (= 10.7.0)
+ - React-Core
+ - RNFBApp
- RNFlashList (1.4.3):
- React-Core
- RNGestureHandler (2.9.0):
@@ -530,6 +551,11 @@ PODS:
- React-Core
- RNInAppBrowser (3.5.1):
- React-Core
+ - RNNotifee (7.7.1):
+ - React-Core
+ - RNNotifee/NotifeeCore (= 7.7.1)
+ - RNNotifee/NotifeeCore (7.7.1):
+ - React-Core
- RNReactNativeHapticFeedback (2.0.3):
- React-Core
- RNReanimated (2.14.4):
@@ -634,16 +660,20 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
+ - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
+ - RNDefaultPreference (from `../node_modules/react-native-default-preference`)
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBAuth (from `../node_modules/@react-native-firebase/auth`)"
- "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)"
+ - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
- "RNFlashList (from `../node_modules/@shopify/flash-list`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNIap (from `../node_modules/react-native-iap`)
- RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
+ - "RNNotifee (from `../node_modules/@notifee/react-native`)"
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
@@ -663,6 +693,7 @@ SPEC REPOS:
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
+ - FirebaseMessaging
- FirebaseSessions
- fmt
- GoogleAppMeasurement
@@ -777,8 +808,12 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCMaskedView:
:path: "../node_modules/@react-native-masked-view/masked-view"
+ RNCPushNotificationIOS:
+ :path: "../node_modules/@react-native-community/push-notification-ios"
RNDateTimePicker:
:path: "../node_modules/@react-native-community/datetimepicker"
+ RNDefaultPreference:
+ :path: "../node_modules/react-native-default-preference"
RNDeviceInfo:
:path: "../node_modules/react-native-device-info"
RNFBAnalytics:
@@ -789,6 +824,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-firebase/auth"
RNFBCrashlytics:
:path: "../node_modules/@react-native-firebase/crashlytics"
+ RNFBMessaging:
+ :path: "../node_modules/@react-native-firebase/messaging"
RNFlashList:
:path: "../node_modules/@shopify/flash-list"
RNGestureHandler:
@@ -797,6 +834,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-iap"
RNInAppBrowser:
:path: "../node_modules/react-native-inappbrowser-reborn"
+ RNNotifee:
+ :path: "../node_modules/@notifee/react-native"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNReanimated:
@@ -832,6 +871,7 @@ SPEC CHECKSUMS:
FirebaseCoreInternal: d2b4acb827908e72eca47a9fd896767c3053921e
FirebaseCrashlytics: 35fdd1a433b31e28adcf5c8933f4c526691a1e0b
FirebaseInstallations: c58489c9caacdbf27d1da60891a87318e20218e0
+ FirebaseMessaging: ac9062bcc35ed56e15a0241d8fd317022499baf8
FirebaseSessions: 5f9e62cd4096e24d2011cbd845b0efceffaee1ec
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
@@ -883,16 +923,20 @@ SPEC CHECKSUMS:
ReactCommon: dbfbe2f7f3c5ce4ce44f43f2fd0d5950d1eb67c5
RNCAsyncStorage: b90b71f45b8b97be43bc4284e71a6af48ac9f547
RNCMaskedView: bc0170f389056201c82a55e242e5d90070e18e5a
+ RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDateTimePicker: 00247f26c34683c80be94207f488f6f13448586e
+ RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 475a4c447168d0ad4c807e48ef5e0963a0f4eb1b
RNFBAnalytics: 275d67a43d550306fd1ec6b5b632670d872f0380
RNFBApp: d59efa0872fff4e27de03cca3c528c203a436ae5
RNFBAuth: 54e598bb1358a0cf6c0a187f43161cd1bde90455
RNFBCrashlytics: f2dd2ad20eff4d5ceaa5c19936efff3b1607eb33
+ RNFBMessaging: 216693dd5ba4f18ba65fb39fc73a44a23c26190f
RNFlashList: ade81b4e928ebd585dd492014d40fb8d0e848aab
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNIap: c397f49db45af3b10dca64b2325f21bb8078ad21
RNInAppBrowser: 48b95ba7a4eaff5cc223bca338d3e319561dbd1b
+ RNNotifee: 05692d7bb42b6c718a3906aeb8431c48ff2e8097
RNReactNativeHapticFeedback: afa5bf2794aecbb2dba2525329253da0d66656df
RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
diff --git a/ios/StationIntent/Info.plist b/ios/StationIntent/Info.plist
index 72b28c75..61abf402 100644
--- a/ios/StationIntent/Info.plist
+++ b/ios/StationIntent/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7
+ 11
NSExtension
NSExtensionAttributes
diff --git a/ios/WatchBetterRail Extension/Info.plist b/ios/WatchBetterRail Extension/Info.plist
index 79526a16..3f9863cc 100644
--- a/ios/WatchBetterRail Extension/Info.plist
+++ b/ios/WatchBetterRail Extension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7
+ 11
CLKComplicationPrincipalClass
$(PRODUCT_MODULE_NAME).ComplicationController
NSExtension
diff --git a/ios/WatchBetterRail/Info.plist b/ios/WatchBetterRail/Info.plist
index 400e6cac..98c21864 100644
--- a/ios/WatchBetterRail/Info.plist
+++ b/ios/WatchBetterRail/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7
+ 11
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
diff --git a/ios/he.lproj/Localizable.strings b/ios/he.lproj/Localizable.strings
index ed389da4..eadda074 100644
--- a/ios/he.lproj/Localizable.strings
+++ b/ios/he.lproj/Localizable.strings
@@ -53,7 +53,6 @@
"thanks for riding with better rail" = "תודה שנסעתם עם בטר רייל!";
"time left depart" = "זמן שנותר ליציאת הרכבת";
"time left arrival" = "זמן שנותר להגעה ליעד";
-"thanks for riding with better rail" = "תודה שבחרתם בטר רייל!";
"BETTER RAIL" = "בטר רייל";
"connection issues" = "בעיות תקשורת";
"%@ min delay" = "%@ דק׳ איחור";
diff --git a/package.json b/package.json
index 3f9e5cd2..98877420 100644
--- a/package.json
+++ b/package.json
@@ -28,14 +28,17 @@
"dependencies": {
"@expo/react-native-action-sheet": "4.0.1",
"@gorhom/bottom-sheet": "^4",
+ "@notifee/react-native": "^7.7.1",
"@react-native-async-storage/async-storage": "1.18.1",
"@react-native-community/blur": "^4.3.2",
"@react-native-community/datetimepicker": "6.7.3",
"@react-native-community/netinfo": "9.3.7",
+ "@react-native-community/push-notification-ios": "^1.11.0",
"@react-native-firebase/analytics": "^17.5.0",
"@react-native-firebase/app": "^17.5.0",
"@react-native-firebase/auth": "^17.5.0",
"@react-native-firebase/crashlytics": "^17.5.0",
+ "@react-native-firebase/messaging": "^17.5.0",
"@react-native-masked-view/masked-view": "0.2.8",
"@react-native-segmented-control/segmented-control": "2.4.0",
"@react-navigation/elements": "^1.3.17",
@@ -61,6 +64,7 @@
"react-native-add-calendar-event": "^4.2.2",
"react-native-bouncy-checkbox": "^3.0.7",
"react-native-date-picker": "4.2.10",
+ "react-native-default-preference": "^1.4.4",
"react-native-device-info": "10.6.0",
"react-native-gesture-handler": "~2.9.0",
"react-native-haptic-feedback": "2.0.3",
@@ -73,6 +77,7 @@
"react-native-modal": "13.0.1",
"react-native-modal-datetime-picker": "9.2.1",
"react-native-prompt-android": "^1.1.0",
+ "react-native-push-notification": "^8.1.1",
"react-native-reanimated": "2.14.4",
"react-native-restart": "0.0.27",
"react-native-safe-area-context": "4.5.0",
@@ -102,6 +107,7 @@
"@types/lodash": "^4.14.194",
"@types/ramda": "0.27.32",
"@types/react": "~18.0.27",
+ "@types/react-native-push-notification": "^8.1.1",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.58.0",
diff --git a/yarn.lock b/yarn.lock
index 7a9a3d7a..9a2cbfdd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3709,6 +3709,11 @@
"@nodelib/fs.scandir" "2.1.3"
fastq "^1.6.0"
+"@notifee/react-native@^7.7.1":
+ version "7.7.1"
+ resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.7.1.tgz#ce3f982fb7354519406cb7716f8e861bab0056ce"
+ integrity sha512-E+W91ulI4dxdIrhK6YCyjWqXgrUsVNZYYCSn3gDADmveuR2Yd2uGvbbSW2vUIFU4N4gQQT/5HJdk9Jk83KHbVA==
+
"@npmcli/fs@^1.0.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257"
@@ -3921,6 +3926,13 @@
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-9.3.7.tgz#92407f679f00bae005c785a9284e61d63e292b34"
integrity sha512-+taWmE5WpBp0uS6kf+bouCx/sn89G9EpR4s2M/ReLvctVIFL2Qh8WnWfBxqK9qwgmFha/uqjSr2Gq03OOtiDcw==
+"@react-native-community/push-notification-ios@^1.11.0":
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/@react-native-community/push-notification-ios/-/push-notification-ios-1.11.0.tgz#d8ec4acfb52260cb779ed0379b9e197db7841b83"
+ integrity sha512-nfkUs8P2FeydOCR4r7BNmtGxAxI22YuGP6RmqWt6c8EEMUpqvIhNKWkRSFF3pHjkgJk2tpRb9wQhbezsqTyBvA==
+ dependencies:
+ invariant "^2.2.4"
+
"@react-native-firebase/analytics@^17.5.0":
version "17.5.0"
resolved "https://registry.yarnpkg.com/@react-native-firebase/analytics/-/analytics-17.5.0.tgz#4f133f64edf2c5959116f862a62c12e6f4ec20ce"
@@ -3948,6 +3960,11 @@
dependencies:
stacktrace-js "^2.0.0"
+"@react-native-firebase/messaging@^17.5.0":
+ version "17.5.0"
+ resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-17.5.0.tgz#9c6db50423c67760dd61e553189187b9e5a2b75e"
+ integrity sha512-9jB7jEt4ySdALio2qwquAkXI106GTdPEuGY+vUFuNT6Wwwc5VQhyiJ5yforaF0BEsppFgK151PLYQQdIhahgzA==
+
"@react-native-masked-view/masked-view@0.2.8":
version "0.2.8"
resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.2.8.tgz#34405a4361882dae7c81b1b771fe9f5fbd545a97"
@@ -4890,6 +4907,11 @@
"@types/history" "*"
"@types/react" "*"
+"@types/react-native-push-notification@^8.1.1":
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/@types/react-native-push-notification/-/react-native-push-notification-8.1.1.tgz#0c9a181d7823cfad215d040bc5596c2d83e1a3cd"
+ integrity sha512-ZN4UbU4EM3C7XGt4zI6RqHEZS2+35EwOz9DPAD1lTTY3IpWMHAKYjryykvP35hFkSwrGMpT8nYuMFPEJRwDEJA==
+
"@types/react-syntax-highlighter@11.0.4":
version "11.0.4"
resolved "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz"
@@ -16258,6 +16280,11 @@ react-native-date-picker@4.2.10:
dependencies:
prop-types "^15.8.1"
+react-native-default-preference@^1.4.4:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/react-native-default-preference/-/react-native-default-preference-1.4.4.tgz#0d9f0a44dfc006f62f4479adc5b1fd68a8baf2e4"
+ integrity sha512-h0vtgiSKws3UmMRJykXAVM4ne1SgfoocUcoBD19ewRpQd6wqurE0HJRQGrSxcHK5LdKE7QPSIB1VX3YGIVS8Jg==
+
react-native-device-info@10.6.0:
version "10.6.0"
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-10.6.0.tgz#51f1b2ec98abc32747149b0a5b7fb662b44d50e4"
@@ -16348,6 +16375,11 @@ react-native-prompt-android@^1.1.0:
resolved "https://registry.npmjs.org/react-native-prompt-android/-/react-native-prompt-android-1.1.0.tgz"
integrity sha512-4JoyEaT2ZnK9IH+tDFpbTiQBgva8UIFGQf4/Uw/tnEVWBERlVlzcs5B82T9BkeEhEqXhp89JaiSBnLWj30lciw==
+react-native-push-notification@^8.1.1:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/react-native-push-notification/-/react-native-push-notification-8.1.1.tgz#a41d20c70ea5a7709417e96261b225461f8dc73a"
+ integrity sha512-XpBtG/w+a6WXTxu6l1dNYyTiHnbgnvjoc3KxPTxYkaIABRmvuJZkFxqruyGvfCw7ELAlZEAJO+dthdTabCe1XA==
+
react-native-reanimated@2.14.4:
version "2.14.4"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836"