From f8aaf696e02ed51cc7b15caaee9bb0ff7ca8a9ba Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Tue, 17 Dec 2024 13:44:25 -0700 Subject: [PATCH] feat(Android): add basic onboarding UX (#592) * feat(Android): add basic onboarding UX * add tests * spin out ContentViewModel * remember sharingLocation as saveable * attempt to fix flaky test this was working always locally and never in CI. weird * fix first location after onboarding being lost * just attach the event handler before firing the event --- .../tid/mbta_app/android/ContentViewTests.kt | 3 +- .../onboarding/OnboardingScreenViewTest.kt | 104 +++++++++++++++++ .../android/pages/OnboardingPageTest.kt | 51 +++++++++ .../mbta/tid/mbta_app/android/ContentView.kt | 17 ++- .../tid/mbta_app/android/ContentViewModel.kt | 31 +++++ .../tid/mbta_app/android/MainApplication.kt | 7 ++ .../android/location/LocationDataManager.kt | 9 +- .../tid/mbta_app/android/map/HomeMapView.kt | 10 +- .../onboarding/OnboardingScreenView.kt | 107 ++++++++++++++++++ .../mbta_app/android/pages/OnboardingPage.kt | 45 ++++++++ .../res/values-b+es/strings_ios_converted.xml | 11 ++ .../res/values-b+ht/strings_ios_converted.xml | 11 ++ .../values-b+pt+BR/strings_ios_converted.xml | 11 ++ .../res/values-b+vi/strings_ios_converted.xml | 11 ++ .../strings_ios_converted.xml | 11 ++ .../strings_ios_converted.xml | 11 ++ androidApp/src/main/res/values/strings.xml | 11 ++ iosApp/iosApp/Localizable.xcstrings | 41 +++++++ .../kotlin/com/mbta/tid/mbta_app/Helpers.kt | 9 +- 19 files changed, 498 insertions(+), 13 deletions(-) create mode 100644 androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenViewTest.kt create mode 100644 androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/pages/OnboardingPageTest.kt create mode 100644 androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentViewModel.kt create mode 100644 androidApp/src/main/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenView.kt create mode 100644 androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/OnboardingPage.kt diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/ContentViewTests.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/ContentViewTests.kt index 1a2c325e6..598b74b1c 100644 --- a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/ContentViewTests.kt +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/ContentViewTests.kt @@ -31,7 +31,8 @@ class ContentViewTests : KoinTest { val koinApplication = koinApplication { modules( repositoriesModule(MockRepositories.buildWithDefaults()), - module { single { MockPhoenixSocket() } } + MainApplication.koinViewModelModule, + module { single { MockPhoenixSocket() } }, ) } diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenViewTest.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenViewTest.kt new file mode 100644 index 000000000..64b7bfb82 --- /dev/null +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenViewTest.kt @@ -0,0 +1,104 @@ +package com.mbta.tid.mbta_app.android.onboarding + +import android.location.Location +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.rule.GrantPermissionRule +import com.mbta.tid.mbta_app.android.location.MockLocationDataManager +import com.mbta.tid.mbta_app.model.OnboardingScreen +import com.mbta.tid.mbta_app.repositories.MockSettingsRepository +import com.mbta.tid.mbta_app.repositories.Settings +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test + +class OnboardingScreenViewTest { + @get:Rule val composeTestRule = createComposeRule() + + // We can't easily mock the permission request, so we grant the permission eagerly. + @get:Rule + val runtimePermissionRule = + GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION) + + @Test + fun testLocationFlow() { + var advanced = false + val locationDataManager = MockLocationDataManager(Location("mock")) + composeTestRule.setContent { + OnboardingScreenView( + screen = OnboardingScreen.Location, + advance = { advanced = true }, + locationDataManager = locationDataManager, + ) + } + + composeTestRule + .onNodeWithText( + "We use your location to show you nearby transit options.", + substring = true + ) + .assertIsDisplayed() + composeTestRule.onNodeWithText("Continue").performClick() + + composeTestRule.waitForIdle() + assertTrue(advanced) + } + + @Test + fun testHideMapsFlow() { + var savedSetting = false + val settingsRepo = + MockSettingsRepository( + settings = mapOf(Settings.HideMaps to false), + onSaveSettings = { + assertEquals(mapOf(Settings.HideMaps to true), it) + savedSetting = true + } + ) + var advanced = false + composeTestRule.setContent { + OnboardingScreenView( + screen = OnboardingScreen.HideMaps, + advance = { advanced = true }, + locationDataManager = MockLocationDataManager(Location("mock")), + settingsRepository = settingsRepo + ) + } + composeTestRule + .onNodeWithText( + "When using TalkBack, we can skip reading out maps to keep you focused on transit information." + ) + .assertIsDisplayed() + composeTestRule.onNodeWithText("Show maps").assertIsDisplayed() + composeTestRule.onNodeWithText("Hide maps").performClick() + + composeTestRule.waitForIdle() + assertTrue(savedSetting) + assertTrue(advanced) + } + + @Test + fun testFeedbackFlow() { + var advanced = false + composeTestRule.setContent { + OnboardingScreenView( + screen = OnboardingScreen.Feedback, + advance = { advanced = true }, + locationDataManager = MockLocationDataManager(Location("mock")), + ) + } + composeTestRule + .onNodeWithText( + "MBTA Go is in the early stages! We want your feedback" + + " as we continue making improvements and adding new features." + ) + .assertIsDisplayed() + composeTestRule.onNodeWithText("Get started").performClick() + + composeTestRule.waitForIdle() + assertTrue(advanced) + } +} diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/pages/OnboardingPageTest.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/pages/OnboardingPageTest.kt new file mode 100644 index 000000000..30c155f6f --- /dev/null +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/pages/OnboardingPageTest.kt @@ -0,0 +1,51 @@ +package com.mbta.tid.mbta_app.android.pages + +import android.location.Location +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.mbta.tid.mbta_app.android.location.MockLocationDataManager +import com.mbta.tid.mbta_app.model.OnboardingScreen +import com.mbta.tid.mbta_app.repositories.MockOnboardingRepository +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test + +class OnboardingPageTest { + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun testFlow() { + val completedScreens = mutableSetOf() + val onboardingRepository = + MockOnboardingRepository( + pendingOnboarding = OnboardingScreen.entries, + onMarkComplete = completedScreens::add + ) + var finished = false + + composeTestRule.setContent { + OnboardingPage( + screens = OnboardingScreen.entries, + locationDataManager = MockLocationDataManager(Location("mock")), + onFinish = { finished = true }, + onboardingRepository = onboardingRepository, + skipLocationDialogue = true, + ) + } + + composeTestRule.onNodeWithText("Continue").performClick() + composeTestRule.waitUntil { completedScreens.size == 1 } + assertEquals(1, completedScreens.size) + + composeTestRule.onNodeWithText("Show maps").performClick() + composeTestRule.waitUntil { completedScreens.size == 2 } + assertEquals(2, completedScreens.size) + + composeTestRule.onNodeWithText("Get started").performClick() + composeTestRule.waitUntil { completedScreens.size == 3 } + assertEquals(OnboardingScreen.entries.toSet(), completedScreens) + assertTrue(finished) + } +} diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt index 9b5c6ae52..022dca695 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -24,22 +25,26 @@ import com.mbta.tid.mbta_app.android.location.rememberLocationDataManager import com.mbta.tid.mbta_app.android.pages.MorePage import com.mbta.tid.mbta_app.android.pages.NearbyTransit import com.mbta.tid.mbta_app.android.pages.NearbyTransitPage +import com.mbta.tid.mbta_app.android.pages.OnboardingPage import com.mbta.tid.mbta_app.android.phoenix.PhoenixSocketWrapper import com.mbta.tid.mbta_app.android.state.getGlobalData import com.mbta.tid.mbta_app.android.state.subscribeToAlerts import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse import com.mbta.tid.mbta_app.network.PhoenixSocket import io.github.dellisd.spatialk.geojson.Position +import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @OptIn(MapboxExperimental::class, ExperimentalMaterial3Api::class) @Composable fun ContentView( socket: PhoenixSocket = koinInject(), + viewModel: ContentViewModel = koinViewModel(), ) { val navController = rememberNavController() - var alertData: AlertsStreamDataResponse? = subscribeToAlerts() + val alertData: AlertsStreamDataResponse? = subscribeToAlerts() val globalResponse = getGlobalData() + val pendingOnboarding = viewModel.pendingOnboarding.collectAsState().value val locationDataManager = rememberLocationDataManager() val mapViewportState = rememberMapViewportState { setCameraOptions { @@ -62,6 +67,16 @@ fun ContentView( (socket as? PhoenixSocketWrapper)?.attachLogging() onDispose { socket.detach() } } + + if (!pendingOnboarding.isNullOrEmpty()) { + OnboardingPage( + pendingOnboarding, + onFinish = { viewModel.clearPendingOnboarding() }, + locationDataManager = locationDataManager + ) + return + } + val sheetModifier = Modifier.fillMaxSize().background(colorResource(id = R.color.fill1)) NavHost(navController = navController, startDestination = Routes.NearbyTransit) { composable { diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentViewModel.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentViewModel.kt new file mode 100644 index 000000000..5ab4f35d7 --- /dev/null +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentViewModel.kt @@ -0,0 +1,31 @@ +package com.mbta.tid.mbta_app.android + +import androidx.lifecycle.ViewModel +import com.mbta.tid.mbta_app.model.OnboardingScreen +import com.mbta.tid.mbta_app.repositories.IOnboardingRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class ContentViewModel(private val onboardingRepository: IOnboardingRepository) : ViewModel() { + private val _pendingOnboarding: MutableStateFlow?> = + MutableStateFlow(null) + val pendingOnboarding: StateFlow?> = _pendingOnboarding + + init { + loadPendingOnboarding() + } + + fun loadPendingOnboarding() { + CoroutineScope(Dispatchers.IO).launch { + val data = onboardingRepository.getPendingOnboarding() + _pendingOnboarding.value = data + } + } + + fun clearPendingOnboarding() { + _pendingOnboarding.value = emptyList() + } +} diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt index d7073e88b..baa36d2a5 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt @@ -6,6 +6,8 @@ import com.mbta.tid.mbta_app.android.util.decodeMessage import com.mbta.tid.mbta_app.dependencyInjection.makeNativeModule import com.mbta.tid.mbta_app.initKoin import com.mbta.tid.mbta_app.repositories.AccessibilityStatusRepository +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module import org.phoenixframework.Socket // unfortunately, expect/actual only works in multiplatform projects, so we can't @@ -19,7 +21,12 @@ class MainApplication : Application() { initKoin( appVariant, makeNativeModule(AccessibilityStatusRepository(applicationContext), socket.wrapped()), + koinViewModelModule, this ) } + + companion object { + val koinViewModelModule = module { viewModelOf(::ContentViewModel) } + } } diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/location/LocationDataManager.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/location/LocationDataManager.kt index 562066f2c..da1924266 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/location/LocationDataManager.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/location/LocationDataManager.kt @@ -81,7 +81,7 @@ open class LocationDataManager { if (hasPermission) { LaunchedEffect(Unit) { locationClient.lastLocation.addOnSuccessListener { location: Location? -> - _currentLocation.tryEmit(location) + _currentLocation.value = location } } @@ -106,7 +106,7 @@ open class LocationDataManager { if (hasPermission && settingsCorrect) { DisposableEffect(locationRequest, lifecycleOwner) { val locationCallback = LocationListener { location -> - _currentLocation.tryEmit(location) + _currentLocation.value = location } val lifecycleObserver = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { @@ -134,12 +134,13 @@ open class LocationDataManager { @OptIn(ExperimentalPermissionsApi::class) @Composable - fun rememberPermissions() = + fun rememberPermissions(onPermissionsResult: (Map) -> Unit = {}) = rememberMultiplePermissionsState( listOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION - ) + ), + onPermissionsResult ) open val currentLocation = _currentLocation.asStateFlow() diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt index 30938794f..92f8035bd 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/map/HomeMapView.kt @@ -258,6 +258,11 @@ fun HomeMapView( val locationProvider = remember { PassthroughLocationProvider() } + MapEffect(true) { map -> + map.mapboxMap.addOnMapClickListener { point -> handleStopClick(map, point) } + map.location.setLocationProvider(locationProvider) + } + LaunchedEffect(locationDataManager) { locationDataManager.currentLocation.collect { location -> if (location != null) { @@ -268,11 +273,6 @@ fun HomeMapView( } } - MapEffect(true) { map -> - map.mapboxMap.addOnMapClickListener { point -> handleStopClick(map, point) } - map.location.setLocationProvider(locationProvider) - } - MapEffect(locationDataManager.hasPermission) { map -> if (locationDataManager.hasPermission && viewportProvider.isDefault()) { viewportProvider.follow( diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenView.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenView.kt new file mode 100644 index 000000000..ff4aee8c7 --- /dev/null +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/onboarding/OnboardingScreenView.kt @@ -0,0 +1,107 @@ +package com.mbta.tid.mbta_app.android.onboarding + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.mbta.tid.mbta_app.android.R +import com.mbta.tid.mbta_app.android.location.LocationDataManager +import com.mbta.tid.mbta_app.model.OnboardingScreen +import com.mbta.tid.mbta_app.repositories.ISettingsRepository +import com.mbta.tid.mbta_app.repositories.Settings +import kotlinx.coroutines.launch +import org.koin.compose.koinInject + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun OnboardingScreenView( + screen: OnboardingScreen, + advance: () -> Unit, + locationDataManager: LocationDataManager, + skipLocationDialogue: Boolean = false, + settingsRepository: ISettingsRepository = koinInject() +) { + val coroutineScope = rememberCoroutineScope() + var sharingLocation by rememberSaveable { mutableStateOf(false) } + val permissions = + locationDataManager.rememberPermissions( + onPermissionsResult = { + // This only fires after the permissions state has changed + if (sharingLocation) { + advance() + } + } + ) + + fun hideMaps(hide: Boolean) { + coroutineScope.launch { + settingsRepository.setSettings(mapOf(Settings.HideMaps to hide)) + advance() + } + } + + fun shareLocation() { + if (skipLocationDialogue || permissions.permissions.any { it.status.isGranted }) { + advance() + } else { + sharingLocation = true + permissions.launchMultiplePermissionRequest() + } + } + + Column { + when (screen) { + OnboardingScreen.Feedback -> { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.Start + ) { + Text(stringResource(R.string.onboarding_feedback_header)) + Text(stringResource(R.string.onboarding_feedback_body)) + Button(onClick = advance) { + Text(stringResource(R.string.onboarding_feedback_advance)) + } + } + } + OnboardingScreen.HideMaps -> { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.Start + ) { + Text(stringResource(R.string.onboarding_hide_maps_header)) + Text(stringResource(R.string.onboarding_hide_maps_body)) + Button(onClick = { hideMaps(true) }) { + Text(stringResource(R.string.onboarding_hide_maps_hide)) + } + Button(onClick = { hideMaps(false) }) { + Text(stringResource(R.string.onboarding_hide_maps_show)) + } + } + } + OnboardingScreen.Location -> { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.Start + ) { + Text(stringResource(R.string.onboarding_location_header)) + Text(stringResource(R.string.onboarding_location_body)) + Button(onClick = ::shareLocation) { + Text(stringResource(R.string.onboarding_location_advance)) + } + Text(stringResource(R.string.onboarding_location_footer)) + } + } + } + } +} diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/OnboardingPage.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/OnboardingPage.kt new file mode 100644 index 000000000..bc252dbfc --- /dev/null +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/OnboardingPage.kt @@ -0,0 +1,45 @@ +package com.mbta.tid.mbta_app.android.pages + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import com.mbta.tid.mbta_app.android.location.LocationDataManager +import com.mbta.tid.mbta_app.android.onboarding.OnboardingScreenView +import com.mbta.tid.mbta_app.model.OnboardingScreen +import com.mbta.tid.mbta_app.repositories.IOnboardingRepository +import kotlinx.coroutines.launch +import org.koin.compose.koinInject + +@Composable +fun OnboardingPage( + screens: List, + locationDataManager: LocationDataManager, + onFinish: () -> Unit, + onAdvance: () -> Unit = {}, + onboardingRepository: IOnboardingRepository = koinInject(), + skipLocationDialogue: Boolean = false +) { + var selectedIndex by remember { mutableIntStateOf(0) } + val coroutineScope = rememberCoroutineScope() + + val screen = screens[selectedIndex] + OnboardingScreenView( + screen, + { + coroutineScope.launch { + onboardingRepository.markOnboardingCompleted(screen) + if (selectedIndex < screens.size - 1) { + selectedIndex += 1 + onAdvance() + } else { + onFinish() + } + } + }, + locationDataManager, + skipLocationDialogue + ) +} diff --git a/androidApp/src/main/res/values-b+es/strings_ios_converted.xml b/androidApp/src/main/res/values-b+es/strings_ios_converted.xml index 1487276ca..161c50dfa 100644 --- a/androidApp/src/main/res/values-b+es/strings_ios_converted.xml +++ b/androidApp/src/main/res/values-b+es/strings_ios_converted.xml @@ -34,6 +34,17 @@ No hay paradas cercanas En dirección norte Ahora + Empezar + MBTA Go está en su etapa inicial. Queremos recibir sus comentarios mientras seguimos realizando mejoras y agregando nuevas características. + Ayúdenos a mejorar + Al usar TalkBack, podemos omitir la lectura de mapas para que usted se concentre en la información de tránsito. + Establecer preferencia de mapa + Ocultar mapas + Mostrar mapas + Continuar + Usamos su ubicación para mostrarle opciones de tráfico cercanas. + Puede cambiar la configuración de ubicación en la aplicación de Configuración. + Ver tráfico cerca de usted Política de privacidad Ver código fuente en GitHub Términos de uso diff --git a/androidApp/src/main/res/values-b+ht/strings_ios_converted.xml b/androidApp/src/main/res/values-b+ht/strings_ios_converted.xml index c5b496939..44aa204bc 100644 --- a/androidApp/src/main/res/values-b+ht/strings_ios_converted.xml +++ b/androidApp/src/main/res/values-b+ht/strings_ios_converted.xml @@ -34,6 +34,17 @@ Pa gen arè ki tou pre Nan direksyon nò Kounye a + Kòmanse + MBTA Go nan etap kòmansman yo! Nou vle kòmantè w pandan n ap kontinye fè amelyorasyon epi ajoute nouvo opsyon. + Ede nou amelyore + Lè w ap itilize TalkBack, nou ka sote lekti kat yo pou kenbe w konsantre sou enfòmasyon transpò. + Defini preferans kat la + Kache kat yo + Montre kat yo + Kontinye + Nou itilize pozisyon w pou montre w opsyon transpò piblik ki toupre yo. + Ou ka toujou chanje paramèt pozisyon yo apre nan paramèt aplikasyon an. + Gade transpò piblik toupre w Règleman sou Vi Prive Gade sous sou GitHub Kondisyon itilizasyon diff --git a/androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml b/androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml index 4a7773921..51c53f905 100644 --- a/androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml +++ b/androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml @@ -34,6 +34,17 @@ Nenhuma parada próxima Sentido norte Agora + Comece aqui + O MBTA Go está no estágio inicial! Queremos seus comentários enquanto continuamos a fazer melhorias e adicionar novos recursos. + Ajude-nos a melhorar + Ao usar o TalkBack, podemos pular a leitura de mapas para manter você focado nas informações de trânsito. + Definir preferência de mapas + Ocultar mapas + Exibir mapas + Continuar + Usamos sua localização para lhe mostrar suas opções de transporte próximas. + Você sempre poderá mudar as configurações de localização mais tarde no aplicativo Configurações. + Ver transporte perto de você Política de Privacidade Exibir origem no GitHub Termos de Uso diff --git a/androidApp/src/main/res/values-b+vi/strings_ios_converted.xml b/androidApp/src/main/res/values-b+vi/strings_ios_converted.xml index 2fdf6e43a..2646d22fc 100644 --- a/androidApp/src/main/res/values-b+vi/strings_ios_converted.xml +++ b/androidApp/src/main/res/values-b+vi/strings_ios_converted.xml @@ -34,6 +34,17 @@ Không có điểm dừng gần đó Về hướng bắc Hiện nay + Bắt đầu + MBTA Go đang trong giai đoạn đầu! Chúng tôi muốn nghe ý kiến đóng góp của bạn để tiếp tục cải thiện và bổ sung các tính năng mới. + Giúp chúng tôi cải thiện + Khi sử dụng TalkBack, chúng ta có thể bỏ qua việc đọc bản đồ để bạn tập trung vào thông tin giao thông. + Đặt tùy chọn bản đồ + Ẩn bản đồ + Hiển thị bản đồ + Tiếp tục + Chúng tôi sử dụng vị trí của bạn để hiển thị cho bạn các lựa chọn phương tiện công cộng ở gần. + Bạn luôn có thể thay đổi cài đặt vị trí sau trong ứng dụng Cài đặt. + Xem phương tiện công cộng gần bạn Chính sách bảo mật Xem nguồn trên GitHub Điều khoản sử dụng diff --git a/androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml b/androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml index dfe850da1..93592f61c 100644 --- a/androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml +++ b/androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml @@ -34,6 +34,17 @@ 附近无站点 北行 现在 + 开始 + MBTA Go尚处于试运行阶段!我们希望收到您的反馈,以便我们继续改进和新增功能。 + 帮助我们改进 + 使用 TalkBack 时,我们可以跳过读地图的步骤,让您专注于交通信息。 + 设置地图偏好 + 隐藏地图 + 显示地图 + 继续 + 我们使用您的位置向您显示附近的交通选择。 + 您可以随时在“设置”应用程序中更改位置设置。 + 查看您附近的交通 隐私政策 在GitHub上查看源代码 使用条款 diff --git a/androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml b/androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml index ae1898817..5aeb2b605 100644 --- a/androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml +++ b/androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml @@ -34,6 +34,17 @@ 附近沒有停靠站 北行 現在 + 開始 + MBTA Go尚處於試運行階段!我們希望收到您的回饋,以便我們繼續改進和新增功能。 + 幫助我們改進 + 使用 TalkBack 時,我們可以跳過讀取地圖,讓您專注於交通資訊。 + 設定地圖偏好 + 隱藏地圖 + 顯示地圖 + 繼續 + 我們使用您的位置向您顯示附近的交通選擇。 + 您可以隨時在「設定」應用程式中變更位置設定。 + 查看您附近的交通 隱私政策 在GitHub上查看原始程式碼 使用條款 diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index 657c79976..0858bd485 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -36,6 +36,17 @@ No nearby stops Northbound Now + Get started + MBTA Go is in the early stages! We want your feedback as we continue making improvements and adding new features. + Help us improve + When using TalkBack, we can skip reading out maps to keep you focused on transit information. + Set map preference + Hide maps + Show maps + Continue + We use your location to show you nearby transit options. + You can always change location settings later in the Settings app. + See transit near you Privacy Policy View source on GitHub Terms of Use diff --git a/iosApp/iosApp/Localizable.xcstrings b/iosApp/iosApp/Localizable.xcstrings index 13948e2bc..b19b06d4e 100644 --- a/iosApp/iosApp/Localizable.xcstrings +++ b/iosApp/iosApp/Localizable.xcstrings @@ -8884,6 +8884,47 @@ } } }, + "When using TalkBack, we can skip reading out maps to keep you focused on transit information." : { + "extractionState" : "manual", + "localizations" : { + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Al usar TalkBack, podemos omitir la lectura de mapas para que usted se concentre en la información de tránsito." + } + }, + "ht" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Lè w ap itilize TalkBack, nou ka sote lekti kat yo pou kenbe w konsantre sou enfòmasyon transpò." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Ao usar o TalkBack, podemos pular a leitura de mapas para manter você focado nas informações de trânsito." + } + }, + "vi" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Khi sử dụng TalkBack, chúng ta có thể bỏ qua việc đọc bản đồ để bạn tập trung vào thông tin giao thông." + } + }, + "zh-Hans-CN" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "使用 TalkBack 时,我们可以跳过读地图的步骤,让您专注于交通信息。" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "使用 TalkBack 時,我們可以跳過讀取地圖,讓您專注於交通資訊。" + } + } + } + }, "When using VoiceOver, we can skip reading out maps to keep you focused on transit information." : { "localizations" : { "es" : { diff --git a/shared/src/androidMain/kotlin/com/mbta/tid/mbta_app/Helpers.kt b/shared/src/androidMain/kotlin/com/mbta/tid/mbta_app/Helpers.kt index ad94fd6fc..b9eacbf79 100644 --- a/shared/src/androidMain/kotlin/com/mbta/tid/mbta_app/Helpers.kt +++ b/shared/src/androidMain/kotlin/com/mbta/tid/mbta_app/Helpers.kt @@ -11,9 +11,14 @@ import org.koin.core.module.Module internal fun createDataStore(context: Context): DataStore = getDataStore(producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath }) -fun initKoin(appVariant: AppVariant, nativeModule: Module, context: Context) { +fun initKoin( + appVariant: AppVariant, + nativeModule: Module, + viewModelModule: Module, + context: Context +) { startKoin { androidContext(context) - modules(appModule(appVariant) + platformModule() + nativeModule) + modules(appModule(appVariant) + platformModule() + nativeModule + viewModelModule) } }