question

alexrivas avatar image
alexrivas asked

Custom Facing Screen usage

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

Clover Android SDKclover android rom
10 |2000

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

0 Answers

·

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Welcome to the
Clover Developer Community