Skip to content

Commit

Permalink
✨ - Better Rail Live for Android (#256)
Browse files Browse the repository at this point in the history
Added support for android devices:

<img width="351" alt="image"
src="https://github.com/better-rail/app/assets/12946462/585bdc90-e103-4993-a41e-065d3dfd0551">


Todo:
- [x] Fix UI issues
- [x] Add localization
- [x] Replace staging env with production

---------

Co-authored-by: Guy Tepper <[email protected]>
  • Loading branch information
planecore and guytepper authored Aug 19, 2023
1 parent db398b4 commit ad9719f
Show file tree
Hide file tree
Showing 63 changed files with 1,280 additions and 376 deletions.
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>

<application
Expand All @@ -24,5 +26,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon" />
</application>
</manifest>
15 changes: 15 additions & 0 deletions android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.1621053"
android:scaleY="1.1621053"
android:translateX="-1.9452631"
android:translateY="-1.3642105">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2c-4,0 -8,0.5 -8,4v9.5C4,17.43 5.57,19 7.5,19L6,20.5v0.5h2.23l2,-2L14,19l2,2h2v-0.5L16.5,19c1.93,0 3.5,-1.57 3.5,-3.5L20,6c0,-3.5 -3.58,-4 -8,-4zM7.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5S6.67,14 7.5,14s1.5,0.67 1.5,1.5S8.33,17 7.5,17zM11,10L6,10L6,6h5v4zM13,10L13,6h5v4h-5zM16.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</group>
</vector>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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__) {
Expand All @@ -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()
Expand All @@ -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) => {
Expand Down
24 changes: 16 additions & 8 deletions app/components/route-details-header/route-details-header.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)

Expand Down Expand Up @@ -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 = () => {
Expand Down
1 change: 1 addition & 0 deletions app/hooks/use-ride-progress/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
27 changes: 8 additions & 19 deletions app/hooks/use-ride-progress/use-ride-progress.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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" }))
}

Expand Down
56 changes: 56 additions & 0 deletions app/hooks/use-ride-progress/utils.ts
Original file line number Diff line number Diff line change
@@ -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]
}
}
9 changes: 6 additions & 3 deletions app/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -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. ",
Expand Down
23 changes: 17 additions & 6 deletions app/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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": {
Expand All @@ -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. ",
Expand Down
24 changes: 18 additions & 6 deletions app/i18n/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"changes": "החלפות",
"delayTime": "דק' עיכוב",
"noTrainsFound": "לא נמצאו רכבות למסלול זה בימים הקרובים",
"sameStationsMessage": "יש לבחור תחנות מוצא ויעד שונות",
"noInternetConnection": "נראה שהמכשיר אינו מחובר לאינטרנט.\n אנא בדקו את חיבור הרשת ונסו שוב.",
"updates": "עדכונים מרכבת ישראל",
"shortRoute": "מסלול קצר"
Expand Down Expand Up @@ -159,7 +160,8 @@
"announcement": {
"title": "בטר רייל לייב",
"subtitle": "הנסיעה ברכבת הולכת להשתנות עם",
"description": "קבלו התראות על איחורי רכבות, זמן הגעה, הוראות נסיעה ועוד - והכל על גבי מסך הנעילה של המכשיר.",
"description": "התראות על איחורי רכבות, זמן הגעה, הוראות נסיעה ועוד - והכל על גבי מסך הנעילה של המכשיר.",
"androidDescription": "התראות על איחורי רכבות, זמן הגעה, הוראות נסיעה ועוד.",
"weMadeAGuide": "הכנו מדריך קצר על השימוש בפיצ׳ר שישנה את הדרך בה אתם נוסעים ברכבת."
},
"startRide": {
Expand All @@ -170,7 +172,9 @@
},
"liveActivity": {
"title": "לייב אקטיביטי",
"androidTitle": "נסיעה פעילה",
"description": "אחרי שהתחלתם נסיעה, היא תתווסף למסך הנעילה של המכשיר ותתעדכן אוטומטית עם איחורים והתקדמות הרכבת.",
"androidDescription": "אחרי שהתחלתם נסיעה, היא תתווסף למסך הנוטיפקציות ותתעדכן אוטומטית עם איחורים והתקדמות הרכבת.",
"tip": "טיפ: אפשר להתחיל את הנסיעה עד שעה לפני שהרכבת יוצאת מהתחנה. ככה תוכלו לדעת אם יש צורך לרוץ על מנת לתפוס את הרכבת, או שאפשר להשאר רגועים."
},
"dynamicIsland": {
Expand All @@ -179,7 +183,7 @@
"description2": "נתוני הנסיעה יופיעו על גבי האי הדינאמי, אליו תוכלו לגשת מכל מקום במכשיר."
},
"supportUs": {
"title": "היי, אנחנו גיא ומתן",
"title": "היי! אנחנו גיא ומתן",
"description1": "אנחנו שני נוסעי רכבת עם אהבה גדולה לפיתוח אפליקציות.",
"description3": "העבודה על בטר רייל כרוכה במאות שעות עבודה, ואנחנו משלמים מכיסינו הפרטי על מנת לממן את פיתוח האפליקצייה.",
"description4": "אם בטר רייל משפרת לכם את חווית הנסיעה ברכבת, אנא תמכו בנו.",
Expand All @@ -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": "זאת בשל העובדה שלוקח למערכת זמן להקצות \"תקציב נוטיפיקציות\" מספק עבור האפליקציה.",
Expand Down
Loading

0 comments on commit ad9719f

Please sign in to comment.