From ad9719f71811c1c3914b44fd2c9701cc72fc801c Mon Sep 17 00:00:00 2001 From: Matan Mashraki <12946462+planecore@users.noreply.github.com> Date: Sat, 19 Aug 2023 15:32:30 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20-=20Better=20Rail=20Live=20for=20An?= =?UTF-8?q?droid=20(#256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added support for android devices: image Todo: - [x] Fix UI issues - [x] Add localization - [x] Replace staging env with production --------- Co-authored-by: Guy Tepper --- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 3 + .../drawable-anydpi-v24/notification_icon.xml | 15 ++ .../res/drawable-hdpi/notification_icon.png | Bin 0 -> 483 bytes .../res/drawable-mdpi/notification_icon.png | Bin 0 -> 366 bytes .../res/drawable-xhdpi/notification_icon.png | Bin 0 -> 615 bytes .../res/drawable-xxhdpi/notification_icon.png | Bin 0 -> 882 bytes app/app.tsx | 16 +- .../route-details-header.tsx | 24 ++- app/hooks/use-ride-progress/index.ts | 1 + .../use-ride-progress/use-ride-progress.ts | 27 +-- app/hooks/use-ride-progress/utils.ts | 56 ++++++ app/i18n/ar.json | 9 +- app/i18n/en.json | 23 ++- app/i18n/he.json | 24 ++- app/i18n/i18n.ts | 23 ++- app/i18n/ru.json | 9 +- app/models/ride/ride.ts | 41 +++- .../active-ride/active-ride-navigator.tsx | 3 +- .../live-activity-announcement-stack.tsx | 35 ++-- .../activity-announcement-screen.android.tsx | 52 +++++ ...x => activity-announcement-screen.ios.tsx} | 0 .../activity-announcement-screen.ts | 9 + app/screens/live-announcement/index.ts | 2 +- .../live-announcement-screen.android.tsx | 53 +++++ .../live-announcement-screen.ios.tsx | 83 ++++++++ .../live-announcement-screen.tsx | 91 +-------- ...start-ride-announcement-screen.android.tsx | 49 +++++ ...=> start-ride-announcement-screen.ios.tsx} | 0 .../live-announcement/start-ride-screen.tsx | 8 + ...support-us-announcement-screen.android.tsx | 123 ++++++++++++ .../support-us-announcement-screen.ios.tsx | 132 +++++++++++++ .../support-us-announcement-screen.tsx | 140 +------------- app/screens/planner/planner-screen-header.tsx | 8 +- app/screens/planner/planner-screen.tsx | 7 +- .../components/start-ride-button.tsx | 98 ++++++---- .../route-details/route-details-screen.tsx | 24 ++- .../components/no-trains-found-msg.tsx | 42 ++-- .../select-station/select-station-screen.tsx | 2 +- app/services/api/index.ts | 5 +- app/services/api/{api.ts => rail-api.ts} | 2 +- .../api/{api.types.ts => rail-api.types.ts} | 0 app/services/api/ride-api.ts | 67 +++++++ app/services/api/route-api.ts | 11 +- app/utils/android-helpers.ts | 181 ++++++++++++++++++ app/utils/helpers/ride-helpers.ts | 34 ++++ assets/live-ride/live-ride-intro-english.png | Bin 0 -> 197072 bytes assets/live-ride/live-ride-intro.png | Bin 0 -> 786761 bytes .../live-ride-notification-english.png | Bin 0 -> 104402 bytes assets/live-ride/live-ride-notification.png | Bin 0 -> 261917 bytes assets/live-ride/start-live-ride-english.mp4 | Bin 0 -> 411442 bytes assets/live-ride/start-live-ride.mp4 | Bin 0 -> 570928 bytes index.js | 7 +- ios/BetterRail.xcodeproj/project.pbxproj | 20 +- ios/BetterRail/Info.plist | 2 +- ios/BetterRailWidget/Info.plist | 2 +- ios/Podfile.lock | 44 +++++ ios/StationIntent/Info.plist | 2 +- ios/WatchBetterRail Extension/Info.plist | 2 +- ios/WatchBetterRail/Info.plist | 2 +- ios/he.lproj/Localizable.strings | 1 - package.json | 6 + yarn.lock | 32 ++++ 63 files changed, 1280 insertions(+), 376 deletions(-) create mode 100644 android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml create mode 100644 android/app/src/main/res/drawable-hdpi/notification_icon.png create mode 100644 android/app/src/main/res/drawable-mdpi/notification_icon.png create mode 100644 android/app/src/main/res/drawable-xhdpi/notification_icon.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/notification_icon.png create mode 100644 app/hooks/use-ride-progress/utils.ts create mode 100644 app/screens/live-announcement/activity-announcement-screen.android.tsx rename app/screens/live-announcement/{activity-announcement-screen.tsx => activity-announcement-screen.ios.tsx} (100%) create mode 100644 app/screens/live-announcement/activity-announcement-screen.ts create mode 100644 app/screens/live-announcement/live-announcement-screen.android.tsx create mode 100644 app/screens/live-announcement/live-announcement-screen.ios.tsx create mode 100644 app/screens/live-announcement/start-ride-announcement-screen.android.tsx rename app/screens/live-announcement/{start-ride-announcement-screen.tsx => start-ride-announcement-screen.ios.tsx} (100%) create mode 100644 app/screens/live-announcement/start-ride-screen.tsx create mode 100644 app/screens/live-announcement/support-us-announcement-screen.android.tsx create mode 100644 app/screens/live-announcement/support-us-announcement-screen.ios.tsx rename app/services/api/{api.ts => rail-api.ts} (94%) rename app/services/api/{api.types.ts => rail-api.types.ts} (100%) create mode 100644 app/services/api/ride-api.ts create mode 100644 app/utils/android-helpers.ts create mode 100644 assets/live-ride/live-ride-intro-english.png create mode 100644 assets/live-ride/live-ride-intro.png create mode 100644 assets/live-ride/live-ride-notification-english.png create mode 100644 assets/live-ride/live-ride-notification.png create mode 100644 assets/live-ride/start-live-ride-english.mp4 create mode 100644 assets/live-ride/start-live-ride.mp4 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 0000000000000000000000000000000000000000..21e0bb51300fa0f1c3c869dd04b801927cbcf30a GIT binary patch literal 483 zcmV<90UZ8`P)*q|en4ax?|24w@-pxwZ=dlW9u2PD9uBJm_k2>bu< z01CO$=$;XgiPn?WvsQhgUL?*=>2nck7_>gLzT`3*iA#=|(F+yMwAQKPm9Pt|nRgV4 zpGs-onVUu8@8nig%y;O}lB$9%neWh{ma2lZ%y&?rH9t|C-E@4-KC9hLE6jJK!^)g} zBHe(NePW);{9_@9c_#CZg&gLY%-{b)jrf0Y)>`)1>%aF8RU-+^ccjC&fcXvzO2d2y z1*KuWgM!j9-$6lXnD3yVG|YEUpnOZbcf87Zk0z!ut$CPN2j)94Zw3?ehb+}XI}t3E z_OqnJ$|BRZhyT9JGWtb2z`jGeSFp(RFI^~oMBcFPfWVF{or9hX`^p+&AiYwWlIIRYoNn9%Rs5tYl Z`2j3{$L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e14d80aa33322f7f0c12bafc26983cc747b3d484 GIT binary patch literal 366 zcmV-!0g?WRP)Od`>TI zlg;KYFteSXN3I?$dKAN|1MHEj2a6uXu<8JN@~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2daf2cc23247537b2ab4e0150a27840a4a541830 GIT binary patch literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(FzI=^IEGZjy`9;Z`^Z7SHQ32( zLCb3eJw@3VD-Ww3FBM)gSnUwfczCJt^1-PF+wHfh{@IjV-8jScq`#Enn*Ztlr1muI z+>Ql$ikSS~ z*1%K~ePCNdF5_*cjBnoJ5ALpCzjW*O^>6;J&Rf-CnBH-uB)owu-=j~Tdz0&I8vzVjm?72V;D(;G7G^Wj`4C#T@Utt^LD1Av|o~Exv^t QfeDDg)78&qol`;+0EvqsxBvhE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d45e27d83a4b6f68451bb13be1dd476dd4b91476 GIT binary patch literal 882 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V7ByhaSW-5dwctR;9~=UwnDE3 zEmIuWFC4tc`0l}r2bUOCRb*vkC-5z31_~XlzI5W@nc}`DTXmDNzs$bK&X&1#_YZ%D z->+7!T4g$^HT+Z?EHAC&wkdk74;kEi9EW$Yhxl`_amV{7uEi}TD?Qy{-67K zf365gJ)0b0VYN(7bmtHGYs{a|`7Jd3r@mFp-TYmoyI9_0x6`&sL1lR{o0r|(BE4p= z{o7~xlel+1_0!&#TswJ3gZZ6>44-2si`W%Ud3M6eJ#a_z)K_g^XYEwz?}?hA``WbF zah~K>h4X8!pJBB$$a3UcbKU1*@C(a*mBGh%pZk3O@gKv-bHcA>AM1#nW8L;>{`zfm z?Ae;j3fD<8mleK~SjF2FoEtDFtNBrgw8-*X3oNd(9bIXr;kVVX5J{@2S>)eS>(VzR z_x%2!mp49_v^R|>LJDYT!_q`bnVjuqdp=9wKk&ZtR=okp$n>06cICU*%k%am#_`ql zTfeDY`{VvhyVC2uhZ=09)>#I6n50j>w zJ&cJLCNDbmdCIK?%Mah#Q?2*rQH?QM3-9yq7Z1E{yg2Rmm%v9H&uWa1@?QjrrAmub zANzgaXPO{lZDVF^+MD~BbP=0gUnqrgIVAWEQwI_EgEq|ubbzkp#+AnXNT9N&!>s$}- zN?ZCN`j?cy`TWnauTMHX+_c_oEp?Z)t96 zQ~Sws%LOPrw|UyVC1#CN+Fn0f!sz>GR{mk1DwCMW%|gD9{;XOh_E=woiRZ_ciMB?- Pyu{$?>gTe~DWM4f>JGar literal 0 HcmV?d00001 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 ( + + + + + + + + +