We want to improve our UX by using this feature but we encounter some issues along the way we want to discuss with the community in search for help. We want to display like a basket item for the customer to see their items in real time each time our application adds one to the basket the employee is seeing. So, both should contain the same items, total price and subtotal as well. Right now the integration we have works for a couple of hours before starting frozen the MINI completely in the Station Pro. Here is the integration code:
- MainViewModel
/** * Process a message to send to the Forward Facing Screen. If is the first message, it will start the connection * with the Forward Facing Screen * @param message the message to send to the Forward Facing Screen */ fun sendMessageToFrowardFacingScreen(message: String, action: Action) { if (message.isNotNullOrEmpty()) { _jsonUpdates = message } viewModelScope.launch { when (action) { RESTORE -> { disconnectFromForwardFacingScreen() if (_jsonUpdates.isNotNullOrEmpty()) { connectToForwardFacingScreenUseCase( Constants.BASKET_ACTIVITY_NAME, "$_jsonUpdates@*@$storeName" ) } else { connectToForwardFacingScreenUseCase( Constants.BASKET_ACTIVITY_NAME, "@*@$storeName" ) } } CLEAN -> { disconnectFromForwardFacingScreen() _jsonUpdates = "" } SHOW -> { if (firstCallForCloverPos) { connectToForwardFacingScreenUseCase( Constants.BASKET_ACTIVITY_NAME, "$message@*@$storeName" ) firstCallForCloverPos = false } else { sendMessageToFrowardFacingScreeUseCase(message) } } } } }
Is a little complex what we have there, let me explain. The application may go to background (employee pressed home) and if that is the case we simply release the connection. Same case but the employee have some items already added, the app should release the connection as well but we saved that info in _jsonUpdates for later so if the employee retakes where it left we establish the connection again and send that _jsonUpdates again so the MINI displays where it was left off. Last case is when the employee sends the payment command, all resources are released.
This is the use case logic for connecting:
class ConnectToForwardFacingScreenUseCase @Inject constructor( private val remoteDeviceConnector: RemoteDeviceConnector, private val analyticsHelper: AnalyticsHelper, ) { suspend operator fun invoke(activityName: String, json: String = "") = withContext( Dispatchers.Default ) { val customerActivityHandler = CustomActivityHandler( remoteDeviceConnector = remoteDeviceConnector, activityName = activityName, json = json, analyticsHelper = analyticsHelper ) customerActivityHandler.startActivityAndListenToChanges() } }
- CustomActivityHandler
class CustomActivityHandler( private val remoteDeviceConnector: RemoteDeviceConnector, private val activityName: String, private val json: String = "", private val onMessageFromActivityLambda: ((MessageFromActivity) -> Unit)? = null, private val onCustomActivityResultLambda: ((CustomActivityResponse) -> Unit)? = null, private val analyticsHelper: AnalyticsHelper, ) { suspend fun startActivityAndListenToChanges() = withContext(Dispatchers.IO) { remoteDeviceConnector.connect() val customActivityRequest = CustomActivityRequest(activityName, json) var status = remoteDeviceConnector.retrieveDeviceStatus(DeviceStatusRequest()) var retryTimes = 5 if (status.state == ExternalDeviceState.IDLE) { remoteDeviceConnector.startCustomActivity( customActivityRequest, object : CustomActivityListener { override fun onMessageFromActivity(message: MessageFromActivity) { onMessageFromActivityLambda?.invoke(message) } override fun onCustomActivityResult(response: CustomActivityResponse) { onCustomActivityResultLambda?.invoke(response) } } ) return@withContext } while (status.state != ExternalDeviceState.IDLE && retryTimes > 0) { val statusResponse = remoteDeviceConnector.resetDevice(ResetDeviceRequest()) analyticsHelper.logEvent( AnalyticsEvent( type = AnalyticsEvent.Types.CFP, extras = listOf( AnalyticsEvent.Param( AnalyticsEvent.ParamKeys.MESSAGE_TO_CFP, "resetDevice('${ statusResponse.reason}','${ statusResponse?.state?.name}','${ statusResponse .isSuccess }')" ) ) ) ) status = remoteDeviceConnector.retrieveDeviceStatus(DeviceStatusRequest()) retryTimes-- } if (status.state == ExternalDeviceState.IDLE) { remoteDeviceConnector.startCustomActivity( customActivityRequest, object : CustomActivityListener { override fun onMessageFromActivity(message: MessageFromActivity) { onMessageFromActivityLambda?.invoke(message) } override fun onCustomActivityResult(response: CustomActivityResponse) { onCustomActivityResultLambda?.invoke(response) } } ) } analyticsHelper.logEvent( AnalyticsEvent( type = AnalyticsEvent.Types.CFP, extras = listOf( AnalyticsEvent.Param( AnalyticsEvent.ParamKeys.MESSAGE_TO_CFP, "Creating a new CFP($activityName) -> $json" ) ) ) ) } }
Here is another of the aspects we found a little undocumented. The devices is normally in IDLE mode but sometimes resetDevice doesn't release it at all that's why we retry it 5 times. If there is a better way to achieve we are glad to hear it.
Finally, the custom activity:
@ExcludeGenerated class CustomerActivity : AppCompatActivity(), MessageListener { private var activityHelper: CloverCFPActivityHelper? = null private var comHelper: CloverCFPCommsHelper? = null private var adapter: BasketItemAdapter? = null private lateinit var binding: ActivityCustomerBinding private val analyticsHelper: AnalyticsHelper = StubAnalyticsHelper() override fun onCreate(savedInstance: Bundle?) { super.onCreate(savedInstance) binding = ActivityCustomerBinding.inflate(layoutInflater) setContentView(binding.root) initAdapter() setSystemUiVisibility() activityHelper = CloverCFPActivityHelper(this) comHelper = CloverCFPCommsHelper(this, intent, this) val initialPayload = activityHelper?.initialPayload initialPayload?.let { processPayload(it) } } private fun initAdapter() { adapter = BasketItemAdapter(ArrayList()) val linearLayoutManager = LinearLayoutManager(this) binding.recyclerViewBasketItems.layoutManager = linearLayoutManager binding.recyclerViewBasketItems.adapter = adapter } public override fun onDestroy() { activityHelper?.dispose() comHelper?.dispose() super.onDestroy() } private fun finishWithPayloadToPOS(resultPayload: String? = null) { activityHelper?.setResultAndFinish(RESULT_OK, resultPayload) } override fun onMessage(payload: String) { // called when remoteDeviceConnector.sendMessageToActivity(...) // is invoked from POS analyticsHelper.logEvent( AnalyticsEvent( AnalyticsEvent.Types.CFP, extras = listOf( AnalyticsEvent.Param(AnalyticsEvent.ParamKeys.MESSAGE_TO_CFP, payload), ) ) ) processPayload(payload) } private fun processPayload(payload: String) { if ("FINISH" == payload) { finishWithPayloadToPOS() } else { var basket = payload if (payload.contains("@*@")) { val index = payload.indexOf("@*@") val storeName = try { payload.substring(index + 3) } catch (e: Exception) { "" } basket = payload.substring(0, index) binding.includeRideSideMiniLayout.textViewStoreName.text = storeName } updateUI(basket) } } private fun updateUI(payload: String) { if (payload.isNotEmpty()) { val basket: Basket = fromJson(payload) binding.textViewSubtotalValue.text = basket.subtotal binding.textViewTotalDiscountValue.text = basket.totalDiscount binding.textViewTaxValue.text = basket.taxValue binding.textViewTotalPrice.text = basket.total basket.basketItems?.let { adapter?.setData(it) } binding.recyclerViewBasketItems.scrollToPosition(0) binding.root.transitionToEnd() binding.includeRideSideMiniLayout.imageViewLogo.goneWithFade(600) binding.includeRideSideMiniLayout.textViewStoreName.animateTextSizeChange(18f, 300) } else { binding.root.transitionToStart() binding.includeRideSideMiniLayout.imageViewLogo.visibleWithFade(600) binding.includeRideSideMiniLayout.textViewStoreName.animateTextSizeChange(24f, 300) } } }
That's the complete integration. Again, it DOES work but is not a stable solution we can rely on.
Issues:
- Mini frizzes randomly
- If Mini frizzes, card payments are blocked, solution is to reset the device
- Reset device doesn't work when is frozen