taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

MainActivity.kt (28074B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2020 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.merchantpos
     18 
     19 import android.content.Intent
     20 import android.content.Intent.ACTION_MAIN
     21 import android.content.Intent.CATEGORY_HOME
     22 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
     23 import android.net.Uri
     24 import android.os.Bundle
     25 import android.os.Handler
     26 import android.os.Looper
     27 import android.util.Log
     28 import android.view.View
     29 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
     30 import android.widget.FrameLayout
     31 import android.widget.Toast
     32 import android.widget.Toast.LENGTH_SHORT
     33 import androidx.activity.compose.BackHandler
     34 import androidx.activity.compose.LocalActivity
     35 import androidx.activity.compose.setContent
     36 import androidx.activity.viewModels
     37 import androidx.appcompat.app.AppCompatActivity
     38 import androidx.compose.foundation.layout.padding
     39 import androidx.compose.foundation.layout.fillMaxSize
     40 import androidx.compose.foundation.layout.fillMaxWidth
     41 import androidx.compose.foundation.layout.Column
     42 import androidx.compose.foundation.layout.Row
     43 import androidx.compose.foundation.layout.Arrangement
     44 import androidx.compose.foundation.layout.Spacer
     45 import androidx.compose.foundation.horizontalScroll
     46 import androidx.compose.foundation.layout.heightIn
     47 import androidx.compose.foundation.layout.height
     48 import androidx.compose.foundation.layout.PaddingValues
     49 import androidx.compose.foundation.layout.systemBarsPadding
     50 import androidx.compose.foundation.layout.size
     51 import androidx.compose.foundation.layout.width
     52 import androidx.compose.foundation.rememberScrollState
     53 import androidx.compose.foundation.Image
     54 import androidx.compose.material.icons.Icons
     55 import androidx.compose.material.icons.filled.Menu
     56 import androidx.compose.material3.Button
     57 import androidx.compose.material3.ButtonDefaults
     58 import androidx.compose.material3.DrawerValue
     59 import androidx.compose.material3.ExperimentalMaterial3Api
     60 import androidx.compose.material3.Icon
     61 import androidx.compose.material3.IconButton
     62 import androidx.compose.material3.MaterialTheme
     63 import androidx.compose.material3.ModalDrawerSheet
     64 import androidx.compose.material3.ModalNavigationDrawer
     65 import androidx.compose.material3.NavigationDrawerItem
     66 import androidx.compose.material3.Scaffold
     67 import androidx.compose.material3.Surface
     68 import androidx.compose.material3.Text
     69 import androidx.compose.material3.rememberDrawerState
     70 import androidx.compose.runtime.Composable
     71 import androidx.compose.runtime.DisposableEffect
     72 import androidx.compose.runtime.LaunchedEffect
     73 import androidx.compose.runtime.getValue
     74 import androidx.compose.runtime.remember
     75 import androidx.compose.runtime.rememberCoroutineScope
     76 import androidx.compose.runtime.livedata.observeAsState
     77 import androidx.compose.ui.Modifier
     78 import androidx.compose.ui.res.painterResource
     79 import androidx.compose.ui.platform.LocalContext
     80 import androidx.compose.ui.res.stringResource
     81 import androidx.compose.ui.unit.dp
     82 import androidx.compose.ui.viewinterop.AndroidView
     83 import androidx.fragment.app.Fragment
     84 import androidx.fragment.app.commitNow
     85 import androidx.navigation.NavHostController
     86 import androidx.navigation.compose.NavHost
     87 import androidx.navigation.compose.composable
     88 import androidx.navigation.compose.currentBackStackEntryAsState
     89 import androidx.navigation.compose.rememberNavController
     90 import kotlinx.coroutines.launch
     91 import net.taler.lib.android.TalerNfcService
     92 import net.taler.merchantpos.compose.PosTheme
     93 import net.taler.merchantpos.config.Config
     94 import net.taler.merchantpos.config.ConfigFetcherFragment
     95 import net.taler.merchantpos.config.ConfigFragment
     96 import net.taler.merchantpos.config.GeneralSettingsFragment
     97 import net.taler.merchantpos.history.HistoryFragment
     98 import net.taler.merchantpos.order.OrderFragment
     99 import net.taler.merchantpos.amount.AmountEntryFragment
    100 import net.taler.merchantpos.payment.PaymentSuccessFragment
    101 import net.taler.merchantpos.payment.ProcessPaymentFragment
    102 import net.taler.merchantpos.refund.RefundFragment
    103 import net.taler.merchantpos.refund.RefundUriFragment
    104 import net.taler.merchantpos.order.RestartState
    105 import net.taler.merchantpos.order.RestartState.DISABLED
    106 import net.taler.merchantpos.order.RestartState.UNDO
    107 
    108 class MainActivity : AppCompatActivity() {
    109 
    110     private val model: MainViewModel by viewModels()
    111 
    112     private var navController: NavHostController? = null
    113     private var reallyExit = false
    114 
    115     companion object {
    116         const val TAG = "taler-pos"
    117     }
    118 
    119     override fun onCreate(savedInstanceState: Bundle?) {
    120         super.onCreate(savedInstanceState)
    121 
    122         TalerNfcService.startService(this)
    123 
    124         model.paymentManager.payment.observe(this) { payment ->
    125             payment?.talerPayUri?.let {
    126                 TalerNfcService.setUri(this, it)
    127             } ?: run {
    128                 TalerNfcService.clearNdefPayload(this)
    129             }
    130         }
    131 
    132         model.configManager.sessionExpired.observe(this) {
    133             showPosError(R.string.session_expired_toast)
    134             navigateTo(PosDestination.Config, clearBackStack = true)
    135         }
    136 
    137         setContent {
    138             PosTheme {
    139                 MerchantTerminalApp(
    140                     viewModel = model,
    141                     startDestination = determineStartDestination(),
    142                     onNavControllerReady = { navController = it },
    143                     onExitRequested = ::handleExitRequest,
    144                 )
    145             }
    146         }
    147 
    148         handleSetupIntent(intent)
    149     }
    150 
    151     override fun onStart() {
    152         super.onStart()
    153         if (!model.configManager.config.isValid()) {
    154             navigateTo(PosDestination.Config, clearBackStack = true)
    155         } else if (model.configManager.merchantConfig == null || model.configManager.currency == null) {
    156             navigateTo(PosDestination.ConfigFetcher)
    157         } else {
    158             model.configManager.refreshConfigInBackground()
    159         }
    160     }
    161 
    162     override fun onResume() {
    163         super.onResume()
    164         TalerNfcService.setDefaultHandler(this)
    165     }
    166 
    167     override fun onPause() {
    168         super.onPause()
    169         TalerNfcService.unsetDefaultHandler(this)
    170     }
    171 
    172     override fun onDestroy() {
    173         super.onDestroy()
    174         TalerNfcService.stopService(this)
    175     }
    176 
    177     override fun onNewIntent(intent: Intent) {
    178         super.onNewIntent(intent)
    179         handleSetupIntent(intent)
    180     }
    181 
    182     fun navigateTo(destination: PosDestination, clearBackStack: Boolean = false) {
    183         val controller = navController ?: return
    184         controller.navigate(destination.route) {
    185             launchSingleTop = true
    186             if (clearBackStack) {
    187                 popUpTo(controller.graph.id) {
    188                     inclusive = true
    189                 }
    190             }
    191         }
    192     }
    193 
    194     fun navigateBack() {
    195         val controller = navController ?: return
    196         if (!controller.popBackStack()) {
    197             finish()
    198         }
    199     }
    200 
    201     fun navigateToInitialOrderScreen() {
    202         navigateTo(model.configManager.initialDestination(), clearBackStack = true)
    203     }
    204 
    205     fun handleSetupIntent(intent: Intent) {
    206         if (intent.action != Intent.ACTION_VIEW) return
    207         val data = intent.data ?: return
    208         if (data.scheme != "taler-pos") return
    209 
    210         val host = data.host ?: return
    211         val pathSegments = data.pathSegments
    212         val pathStyleInstance = pathSegments
    213             .takeIf { it.size >= 2 && it[0].equals("instances", ignoreCase = true) }
    214             ?.get(1)
    215             ?.takeIf(String::isNotBlank)
    216         val rawFragment = data.fragment?.removePrefix("/")?.trim().orEmpty()
    217         val params = rawFragment
    218             .takeIf { '=' in it }
    219             ?.split('&')
    220             ?.associate { part ->
    221                 part.split('=', limit = 2).let { it[0] to Uri.decode(it.getOrElse(1) { "" }) }
    222             }
    223 
    224         val instance = pathStyleInstance ?: params?.get("username") ?: return
    225         val token = if (pathStyleInstance != null && params == null) {
    226             Uri.decode(rawFragment).takeIf(String::isNotBlank)
    227         } else {
    228             params?.get("password")
    229         } ?: return
    230 
    231         val merchantUrl = Uri.Builder()
    232             .scheme("https")
    233             .encodedAuthority(host)
    234             .appendPath("instances")
    235             .appendPath(instance)
    236             .build()
    237             .toString()
    238 
    239         val newConfig = Config.New(
    240             merchantUrl = merchantUrl,
    241             accessToken = token,
    242             savePassword = true,
    243         )
    244 
    245         Log.d("MainActivity", "Config URL: $merchantUrl")
    246         model.configManager.config = newConfig
    247         navigateTo(PosDestination.ConfigFetcher)
    248     }
    249 
    250     private fun determineStartDestination(): PosDestination {
    251         return when {
    252             !model.configManager.config.isValid() -> PosDestination.Config
    253             model.configManager.merchantConfig == null || model.configManager.currency == null ->
    254                 PosDestination.ConfigFetcher
    255             else -> model.configManager.initialDestination()
    256         }
    257     }
    258 
    259     private fun handleExitRequest(currentRoute: String?) {
    260         if (currentRoute == PosDestination.Order.route || currentRoute == PosDestination.AmountEntry.route) {
    261             if (reallyExit) {
    262                 super.onBackPressedDispatcher.onBackPressed()
    263             } else {
    264                 reallyExit = true
    265                 Toast.makeText(this, R.string.toast_back_to_exit, LENGTH_SHORT).show()
    266                 Handler(Looper.getMainLooper()).postDelayed({ reallyExit = false }, 3000)
    267             }
    268         } else if (currentRoute == PosDestination.Settings.route || currentRoute == PosDestination.Config.route) {
    269             if (!model.configManager.config.isValid()) {
    270                 startActivity(Intent(ACTION_MAIN).apply {
    271                     addCategory(CATEGORY_HOME)
    272                     flags = FLAG_ACTIVITY_NEW_TASK
    273                 })
    274             } else {
    275                 navigateBack()
    276             }
    277         } else {
    278             navigateBack()
    279         }
    280     }
    281 }
    282 
    283 @OptIn(ExperimentalMaterial3Api::class)
    284 @Composable
    285 private fun MerchantTerminalApp(
    286     viewModel: MainViewModel,
    287     startDestination: PosDestination,
    288     onNavControllerReady: (NavHostController) -> Unit,
    289     onExitRequested: (String?) -> Unit,
    290 ) {
    291     val navController = rememberNavController()
    292     val drawerState = rememberDrawerState(DrawerValue.Closed)
    293     val scope = rememberCoroutineScope()
    294     val backStackEntry by navController.currentBackStackEntryAsState()
    295     val currentRoute = backStackEntry?.destination?.route
    296     val currentOrderId by viewModel.orderManager.currentOrderId.observeAsState()
    297     val currentOrderLive = remember(currentOrderId) { currentOrderId?.let { viewModel.orderManager.getOrder(it) } }
    298     val currentOrderState = currentOrderLive?.order?.observeAsState()
    299     val currentOrder = currentOrderState?.value
    300     val restartState by currentOrderLive?.restartState?.observeAsState(DISABLED) ?: remember { androidx.compose.runtime.mutableStateOf(DISABLED) }
    301     val clearOrderEnabled =
    302         restartState != DISABLED || currentOrder?.products?.isNotEmpty() == true
    303     val hasPreviousOrder = currentOrderId?.let { viewModel.orderManager.hasPreviousOrder(it) } ?: false
    304     val hasNextOrder by currentOrderId?.let { viewModel.orderManager.hasNextOrder(it).observeAsState(false) }
    305         ?: remember { androidx.compose.runtime.mutableStateOf(false) }
    306     val preferredInitialDestination by viewModel.configManager.initialOrderScreenLiveData.observeAsState(
    307         viewModel.configManager.initialOrderScreen
    308     )
    309     val context = LocalContext.current
    310     val hasValidConfig = viewModel.configManager.config.isValid()
    311     val lockMerchantSettingsNavigation =
    312         !hasValidConfig &&
    313             (currentRoute == PosDestination.Settings.route || currentRoute == PosDestination.Config.route)
    314     val screenTitle = when (currentRoute) {
    315         PosDestination.Order.route -> currentOrder?.let {
    316             stringResource(R.string.order_label_title, it.title)
    317         } ?: stringResource(R.string.menu_order)
    318         else -> stringResource(currentRoute.titleResId())
    319     }
    320     val reloadingMessage = stringResource(R.string.toast_reloading)
    321 
    322     val drawerItems = listOf(
    323         PosDestination.AmountEntry,
    324         PosDestination.Order,
    325         PosDestination.History,
    326         PosDestination.Settings,
    327     ).let { items ->
    328         val preferredItem = when (preferredInitialDestination) {
    329             net.taler.merchantpos.config.InitialOrderScreen.AmountEntry -> PosDestination.AmountEntry
    330             net.taler.merchantpos.config.InitialOrderScreen.Inventory -> PosDestination.Order
    331             null -> viewModel.configManager.initialDestination()
    332         }
    333         if (preferredItem in items) {
    334             listOf(preferredItem) + items.filterNot { it == preferredItem }
    335         } else {
    336             items
    337         }
    338     }
    339 
    340     LaunchedEffect(navController) {
    341         onNavControllerReady(navController)
    342     }
    343 
    344     BackHandler {
    345         if (drawerState.isOpen) {
    346             scope.launch { drawerState.close() }
    347         } else {
    348             onExitRequested(currentRoute)
    349         }
    350     }
    351 
    352     ModalNavigationDrawer(
    353         drawerState = drawerState,
    354         gesturesEnabled = !lockMerchantSettingsNavigation,
    355         drawerContent = {
    356             ModalDrawerSheet {
    357                 Column(
    358                     modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
    359                     verticalArrangement = Arrangement.spacedBy(6.dp),
    360                 ) {
    361                     Image(
    362                         painter = painterResource(R.drawable.ic_talerpos_logo),
    363                         contentDescription = null,
    364                         modifier = Modifier
    365                             .fillMaxWidth()
    366                             .height(72.dp)
    367                             .padding(horizontal = 8.dp, vertical = 8.dp),
    368                     )
    369                     drawerItems.forEach { item ->
    370                         NavigationDrawerItem(
    371                             icon = {
    372                                 Icon(
    373                                     painter = painterResource(item.drawerIconResId()),
    374                                     contentDescription = null,
    375                                 )
    376                             },
    377                             label = {
    378                                 Text(
    379                                     text = stringResource(item.labelResId()),
    380                                     fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
    381                                 )
    382                             },
    383                             selected = currentRoute == item.route,
    384                             onClick = {
    385                                 scope.launch { drawerState.close() }
    386                                 navController.navigate(item.route) {
    387                                     launchSingleTop = true
    388                                     popUpTo(navController.graph.startDestinationId) {
    389                                         inclusive = false
    390                                     }
    391                                 }
    392                             },
    393                             modifier = Modifier.fillMaxWidth(),
    394                             shape = MaterialTheme.shapes.large,
    395                             badge = null,
    396                             colors = androidx.compose.material3.NavigationDrawerItemDefaults.colors(),
    397                         )
    398                     }
    399                 }
    400             }
    401         },
    402     ) {
    403         Scaffold(
    404             topBar = {
    405                 if (currentRoute == PosDestination.Order.route) {
    406                     Surface(shadowElevation = 2.dp) {
    407                         Row(
    408                             modifier = Modifier
    409                                 .fillMaxWidth()
    410                                 .systemBarsPadding()
    411                                 .padding(horizontal = 8.dp, vertical = 8.dp),
    412                             horizontalArrangement = Arrangement.spacedBy(8.dp),
    413                             verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
    414                         ) {
    415                             if (!lockMerchantSettingsNavigation) {
    416                                 IconButton(onClick = { scope.launch { drawerState.open() } }) {
    417                                     Icon(Icons.Default.Menu, contentDescription = null)
    418                                 }
    419                             } else {
    420                                 Spacer(modifier = Modifier.size(48.dp))
    421                             }
    422                             Text(
    423                                 text = screenTitle,
    424                                 modifier = Modifier.weight(1f),
    425                                 style = MaterialTheme.typography.titleLarge,
    426                             )
    427                             Row(
    428                                 modifier = Modifier.horizontalScroll(rememberScrollState()),
    429                                 horizontalArrangement = Arrangement.spacedBy(8.dp),
    430                             ) {
    431                                 Button(
    432                                     onClick = { currentOrderLive?.restartOrUndo() },
    433                                     enabled = clearOrderEnabled,
    434                                     colors = topBarOrderButtonColors(),
    435                                     modifier = Modifier.heightIn(min = 40.dp),
    436                                 ) {
    437                                     Text(
    438                                         if (restartState == UNDO) {
    439                                             stringResource(R.string.order_undo)
    440                                         } else {
    441                                             stringResource(R.string.order_restart)
    442                                         },
    443                                     )
    444                                 }
    445                                 Button(
    446                                     onClick = { if (hasPreviousOrder) viewModel.orderManager.previousOrder() },
    447                                     enabled = hasPreviousOrder,
    448                                     colors = topBarOrderButtonColors(),
    449                                     modifier = Modifier.heightIn(min = 40.dp),
    450                                 ) {
    451                                     Text(stringResource(R.string.order_previous))
    452                                 }
    453                                 Button(
    454                                     onClick = { if (hasNextOrder) viewModel.orderManager.nextOrder() },
    455                                     enabled = hasNextOrder,
    456                                     colors = topBarOrderButtonColors(),
    457                                     modifier = Modifier.heightIn(min = 40.dp),
    458                                 ) {
    459                                     Text(stringResource(R.string.order_next))
    460                                 }
    461                                 Button(
    462                                     onClick = {
    463                                         viewModel.configManager.reloadConfig()
    464                                         Toast.makeText(
    465                                             context,
    466                                             reloadingMessage,
    467                                             Toast.LENGTH_LONG,
    468                                         ).show()
    469                                     },
    470                                     colors = topBarOrderButtonColors(),
    471                                     modifier = Modifier.heightIn(min = 40.dp),
    472                                 ) {
    473                                     Text(stringResource(R.string.menu_reload))
    474                                 }
    475                             }
    476                         }
    477                     }
    478                 } else if (currentRoute == PosDestination.History.route) {
    479                     Surface(shadowElevation = 2.dp) {
    480                         Row(
    481                             modifier = Modifier
    482                                 .fillMaxWidth()
    483                                 .systemBarsPadding()
    484                                 .padding(horizontal = 8.dp, vertical = 8.dp),
    485                             horizontalArrangement = Arrangement.spacedBy(8.dp),
    486                             verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
    487                         ) {
    488                             if (!lockMerchantSettingsNavigation) {
    489                                 IconButton(onClick = { scope.launch { drawerState.open() } }) {
    490                                     Icon(Icons.Default.Menu, contentDescription = null)
    491                                 }
    492                             } else {
    493                                 Spacer(modifier = Modifier.size(48.dp))
    494                             }
    495                             Text(
    496                                 text = screenTitle,
    497                                 modifier = Modifier.weight(1f),
    498                                 style = MaterialTheme.typography.titleLarge,
    499                             )
    500                             Button(
    501                                 onClick = { viewModel.historyManager.fetchHistory() },
    502                                 colors = topBarOrderButtonColors(),
    503                                 modifier = Modifier.heightIn(min = 40.dp),
    504                             ) {
    505                                 Text(stringResource(R.string.history_refresh))
    506                             }
    507                         }
    508                     }
    509                 } else {
    510                     Surface(shadowElevation = 2.dp) {
    511                         Row(
    512                             modifier = Modifier
    513                                 .fillMaxWidth()
    514                                 .systemBarsPadding()
    515                                 .padding(horizontal = 8.dp, vertical = 8.dp),
    516                             horizontalArrangement = Arrangement.spacedBy(8.dp),
    517                             verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
    518                         ) {
    519                             if (!lockMerchantSettingsNavigation) {
    520                                 IconButton(onClick = { scope.launch { drawerState.open() } }) {
    521                                     Icon(Icons.Default.Menu, contentDescription = null)
    522                                 }
    523                             } else {
    524                                 Spacer(modifier = Modifier.size(48.dp))
    525                             }
    526                             Text(
    527                                 text = screenTitle,
    528                                 modifier = Modifier.weight(1f),
    529                                 style = MaterialTheme.typography.titleLarge,
    530                             )
    531                         }
    532                     }
    533                 }
    534             },
    535         ) { innerPadding ->
    536             NavHost(
    537                 navController = navController,
    538                 startDestination = startDestination.route,
    539                 modifier = Modifier
    540                     .fillMaxSize()
    541                     .padding(innerPadding),
    542             ) {
    543                 composable(PosDestination.AmountEntry.route) {
    544                     FragmentScreenHost("amount-entry") { AmountEntryFragment() }
    545                 }
    546                 composable(PosDestination.Order.route) {
    547                     FragmentScreenHost("order") { OrderFragment() }
    548                 }
    549                 composable(PosDestination.History.route) {
    550                     FragmentScreenHost("history") { HistoryFragment() }
    551                 }
    552                 composable(PosDestination.Settings.route) {
    553                     FragmentScreenHost("settings") { GeneralSettingsFragment() }
    554                 }
    555                 composable(PosDestination.Config.route) {
    556                     FragmentScreenHost("config") { ConfigFragment() }
    557                 }
    558                 composable(PosDestination.ConfigFetcher.route) {
    559                     FragmentScreenHost("config-fetcher") { ConfigFetcherFragment() }
    560                 }
    561                 composable(PosDestination.ProcessPayment.route) {
    562                     FragmentScreenHost("process-payment") { ProcessPaymentFragment() }
    563                 }
    564                 composable(PosDestination.PaymentSuccess.route) {
    565                     FragmentScreenHost("payment-success") { PaymentSuccessFragment() }
    566                 }
    567                 composable(PosDestination.Refund.route) {
    568                     FragmentScreenHost("refund") { RefundFragment() }
    569                 }
    570                 composable(PosDestination.RefundUri.route) {
    571                     FragmentScreenHost("refund-uri") { RefundUriFragment() }
    572                 }
    573             }
    574         }
    575     }
    576 }
    577 
    578 @Composable
    579 private fun topBarOrderButtonColors() = ButtonDefaults.buttonColors(
    580     containerColor = MaterialTheme.colorScheme.secondaryContainer,
    581     contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
    582 )
    583 
    584 @Composable
    585 private fun FragmentScreenHost(
    586     routeTag: String,
    587     createFragment: () -> Fragment,
    588 ) {
    589     val activity = LocalActivity.current as? MainActivity ?: return
    590     val fragmentManager = activity.supportFragmentManager
    591     val containerId = remember(routeTag) { View.generateViewId() }
    592     val fragmentTag = remember(routeTag, containerId) { "$routeTag-$containerId" }
    593 
    594     AndroidView(
    595         modifier = Modifier.fillMaxSize(),
    596         factory = { context ->
    597             FrameLayout(context).apply {
    598                 id = containerId
    599                 layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
    600             }
    601         },
    602     )
    603 
    604     DisposableEffect(fragmentTag) {
    605         if (fragmentManager.findFragmentByTag(fragmentTag) == null) {
    606             fragmentManager.commitNow {
    607                 replace(containerId, createFragment(), fragmentTag)
    608             }
    609         }
    610         onDispose {
    611             fragmentManager.findFragmentByTag(fragmentTag)?.let { fragment ->
    612                 if (fragment.isAdded && !fragmentManager.isStateSaved) {
    613                     fragmentManager.commitNow {
    614                         remove(fragment)
    615                     }
    616                 }
    617             }
    618         }
    619     }
    620 }
    621 
    622 private fun PosDestination.labelResId(): Int = when (this) {
    623     PosDestination.AmountEntry -> R.string.menu_amount_entry
    624     PosDestination.Order -> R.string.menu_order
    625     PosDestination.History -> R.string.menu_history
    626     PosDestination.Settings -> R.string.menu_settings
    627     else -> R.string.app_name_short
    628 }
    629 
    630 private fun PosDestination.drawerIconResId(): Int = when (this) {
    631     PosDestination.AmountEntry -> R.drawable.ic_dialpad
    632     PosDestination.Order -> R.drawable.ic_move_money_24dp
    633     PosDestination.History -> R.drawable.ic_history_black_24dp
    634     PosDestination.Settings -> R.drawable.ic_menu_manage
    635     else -> R.drawable.ic_move_money_24dp
    636 }
    637 
    638 private fun String?.titleResId(): Int = when (this) {
    639     PosDestination.AmountEntry.route -> R.string.menu_amount_entry
    640     PosDestination.Order.route -> R.string.menu_order
    641     PosDestination.History.route -> R.string.menu_history
    642     PosDestination.Settings.route -> R.string.menu_settings
    643     PosDestination.Config.route -> R.string.config_label
    644     PosDestination.ConfigFetcher.route -> R.string.config_fetching_label
    645     PosDestination.ProcessPayment.route -> R.string.payment_process_label
    646     PosDestination.PaymentSuccess.route -> R.string.payment_received
    647     PosDestination.Refund.route, PosDestination.RefundUri.route -> R.string.history_refund
    648     else -> R.string.app_name_short
    649 }