Skip to content

Commit

Permalink
Merge pull request #13642 from woocommerce/13596-woo-posipp-generic-e…
Browse files Browse the repository at this point in the history
…rror-when-low-batter-during-connection

[IPP] Generic error when low batter during connection
  • Loading branch information
kidinov authored Mar 10, 2025
2 parents 4209fac + 6718e41 commit 1d8ab75
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 19 deletions.
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*** For entries which are touching the Android Wear app's, start entry with `[WEAR]` too.
22.0
-----

- [*] Payments: display specific error for the cases when a reader with low battery level attempted to connect [https://github.com/woocommerce/woocommerce-android/pull/13642]

21.9
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,10 @@ class CardReaderConnectViewModel @Inject constructor(
is CardReaderStatus.Connected -> onReaderConnected(status.cardReader)
is CardReaderStatus.NotConnected -> {
if (connectionStarted) {
status.errorMessage?.let { triggerEvent(ShowToastString(it)) }
onReaderConnectionFailed()
onReaderConnectionFailed(
errorCode = status.errorCode,
errorMessage = status.errorMessage
)
} else {
Unit
}
Expand Down Expand Up @@ -480,10 +482,26 @@ class CardReaderConnectViewModel @Inject constructor(
}
}

private fun onReaderConnectionFailed() {
private fun onReaderConnectionFailed(
errorCode: CardReaderStatus.NotConnected.ErrorCode? = null,
errorMessage: String? = null
) {
tracker.trackConnectionFailed()
WooLog.e(WooLog.T.CARD_READER, "Connecting to reader failed.")
viewState.value = ConnectingFailedState(::restartFlow, ::onCancelClicked)
val hintLabel = when (errorCode) {
CardReaderStatus.NotConnected.ErrorCode.BATTERY_CRITICALLY_LOW ->
R.string.card_reader_connect_failed_battery_low_hint
CardReaderStatus.NotConnected.ErrorCode.OTHER -> null
null -> null
}
if (hintLabel == null && errorMessage != null) {
triggerEvent(ShowToastString(errorMessage))
}
viewState.value = ConnectingFailedState(
onPrimaryActionClicked = ::restartFlow,
onSecondaryActionClicked = ::onCancelClicked,
hintLabel = hintLabel
)
cardReaderOnboardingChecker.invalidateCache()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ sealed interface ConnectingState
sealed class CardReaderConnectViewState(
val headerLabel: UiString? = null,
@DrawableRes val illustration: Int? = null,
@StringRes val hintLabel: Int? = null,
@StringRes open val hintLabel: Int? = null,
val primaryActionLabel: Int? = null,
val secondaryActionLabel: Int? = null,
val tertiaryActionLabel: Int? = null,
Expand Down Expand Up @@ -105,7 +105,8 @@ sealed class CardReaderConnectViewState(

data class ConnectingFailedState(
override val onPrimaryActionClicked: () -> Unit,
override val onSecondaryActionClicked: () -> Unit
override val onSecondaryActionClicked: () -> Unit,
override val hintLabel: Int?
) : CardReaderConnectViewState(
headerLabel = UiString.UiStringRes(R.string.card_reader_connect_failed_header),
illustration = R.drawable.img_products_error,
Expand Down
1 change: 1 addition & 0 deletions WooCommerce/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,7 @@
<string name="card_reader_connect_missing_bluetooth_permissions_header">Missing required nearby devices permission</string>
<string name="card_reader_connect_missing_bluetooth_permission_button">Open settings</string>
<string name="card_reader_connect_learn_more">&lt;a href=\'\'&gt;Learn more&lt;/a&gt; about In-Person Payments</string>
<string name="card_reader_connect_failed_battery_low_hint">The reader battery is low. Please charge the reader and try again</string>

<!--
Card Reader Detail
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,7 @@ class CardReaderConnectViewModelTest : BaseUnitTest() {
(viewModel.event.value as CheckLocationPermissions).onLocationPermissionsCheckResult(true, false)
(viewModel.event.value as CheckLocationEnabled).onLocationEnabledCheckResult(false)

(viewModel.viewStateData.value as? LocationDisabledError)?.let {
it.onPrimaryActionClicked.invoke()
}
(viewModel.viewStateData.value as? LocationDisabledError)?.onPrimaryActionClicked?.invoke()

assertThat(viewModel.event.value).isInstanceOf(OpenLocationSettings::class.java)
}
Expand All @@ -285,9 +283,7 @@ class CardReaderConnectViewModelTest : BaseUnitTest() {
testBlocking {
(viewModel.event.value as CheckLocationPermissions).onLocationPermissionsCheckResult(true, false)
(viewModel.event.value as CheckLocationEnabled).onLocationEnabledCheckResult(false)
(viewModel.viewStateData.value as? LocationDisabledError)?.let {
it.onPrimaryActionClicked.invoke()
}
(viewModel.viewStateData.value as? LocationDisabledError)?.onPrimaryActionClicked?.invoke()

(viewModel.event.value as OpenLocationSettings).onLocationSettingsClosed()

Expand Down Expand Up @@ -971,7 +967,7 @@ class CardReaderConnectViewModelTest : BaseUnitTest() {

(viewModel.viewStateData.value as ExternalReaderFoundState).onPrimaryActionClicked.invoke()
readerStatusFlow.emit(CardReaderStatus.Connecting)
readerStatusFlow.emit(CardReaderStatus.NotConnected(errorMessage))
readerStatusFlow.emit(CardReaderStatus.NotConnected(errorMessage = errorMessage))

assertThat(viewModel.event.value).isEqualTo(ShowToastString(errorMessage))
}
Expand Down Expand Up @@ -1563,6 +1559,77 @@ class CardReaderConnectViewModelTest : BaseUnitTest() {
verify(cardReaderManager).disconnectReader()
}

@Test
fun `given battery critically low error, when connecting fails, then hint label shows battery low message`() =
testBlocking {
init()

(viewModel.viewStateData.value as ExternalReaderFoundState).onPrimaryActionClicked.invoke()
readerStatusFlow.emit(CardReaderStatus.Connecting)
readerStatusFlow.emit(
CardReaderStatus.NotConnected(
errorCode = CardReaderStatus.NotConnected.ErrorCode.BATTERY_CRITICALLY_LOW,
errorMessage = "Battery critically low"
)
)

val state = viewModel.viewStateData.value as ConnectingFailedState
assertThat(state.hintLabel).isEqualTo(R.string.card_reader_connect_failed_battery_low_hint)
}

@Test
fun `given other error, when connecting fails, then hint label is null`() =
testBlocking {
init()

(viewModel.viewStateData.value as ExternalReaderFoundState).onPrimaryActionClicked.invoke()
readerStatusFlow.emit(CardReaderStatus.Connecting)
readerStatusFlow.emit(
CardReaderStatus.NotConnected(
errorCode = CardReaderStatus.NotConnected.ErrorCode.OTHER,
errorMessage = "Other error"
)
)

val state = viewModel.viewStateData.value as ConnectingFailedState
assertThat(state.hintLabel).isNull()
}

@Test
fun `given battery low error with message, when connecting fails, then toast is not shown`() =
testBlocking {
init()

(viewModel.viewStateData.value as ExternalReaderFoundState).onPrimaryActionClicked.invoke()
readerStatusFlow.emit(CardReaderStatus.Connecting)
readerStatusFlow.emit(
CardReaderStatus.NotConnected(
errorCode = CardReaderStatus.NotConnected.ErrorCode.BATTERY_CRITICALLY_LOW,
errorMessage = "Battery critically low"
)
)

assertThat(viewModel.event.value).isNotInstanceOf(ShowToastString::class.java)
}

@Test
fun `given other error with message, when connecting fails, then toast shows error message`() =
testBlocking {
val errorMessage = "Other error message"
init()

(viewModel.viewStateData.value as ExternalReaderFoundState).onPrimaryActionClicked.invoke()
readerStatusFlow.emit(CardReaderStatus.Connecting)
readerStatusFlow.emit(
CardReaderStatus.NotConnected(
errorCode = CardReaderStatus.NotConnected.ErrorCode.OTHER,
errorMessage = errorMessage
)
)

assertThat(viewModel.event.value).isEqualTo(ShowToastString(errorMessage))
}

private fun initVM(
cardReaderFlowParam: CardReaderFlowParam = CardReaderFlowParam.CardReadersHub(),
cardReaderType: CardReaderType = EXTERNAL
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.woocommerce.android.cardreader.connection

sealed class CardReaderStatus {
data class NotConnected(val errorMessage: String? = null) : CardReaderStatus()
data class NotConnected(
val errorCode: ErrorCode? = null,
val errorMessage: String? = null
) : CardReaderStatus() {
enum class ErrorCode {
BATTERY_CRITICALLY_LOW,
OTHER,
}
}
data class Connected(val cardReader: CardReader) : CardReaderStatus()
data object Connecting : CardReaderStatus()
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,12 @@ internal class ConnectionManager(
}

override fun onFailure(e: TerminalException) {
updateReaderStatus(CardReaderStatus.NotConnected(e.errorMessage))
updateReaderStatus(
CardReaderStatus.NotConnected(
errorCode = e.errorCode.toErrorCode(),
errorMessage = e.errorMessage,
)
)
}
}

Expand Down Expand Up @@ -211,4 +216,12 @@ internal class ConnectionManager(
readerCallback
)
}

private fun TerminalException.TerminalErrorCode.toErrorCode(): CardReaderStatus.NotConnected.ErrorCode =
when (this) {
TerminalException.TerminalErrorCode.READER_BATTERY_CRITICALLY_LOW ->
CardReaderStatus.NotConnected.ErrorCode.BATTERY_CRITICALLY_LOW

else -> CardReaderStatus.NotConnected.ErrorCode.OTHER
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() {
on { cardReader }.thenReturn(reader)
}
whenever(terminalWrapper.connectToReader(any(), any(), any(), any())).thenAnswer {
(it.arguments[2] as ReaderCallback).onFailure(mock())
(it.arguments[2] as ReaderCallback).onSuccess(mock())
}

connectionManager.startConnectionToReader(cardReader, "location_id")
Expand All @@ -253,7 +253,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() {
}

@Test
fun `given reader with location id, when connectToReader fails, then status updated with not connected`() =
fun `given reader with location id, when connectToReader fails, then status updated with not connected and other error code`() =
testBlocking {
val reader: Reader = mock {
on { deviceType }.thenReturn(DeviceType.CHIPPER_2X)
Expand All @@ -262,16 +262,52 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() {
on { cardReader }.thenReturn(reader)
}
val message = "error_message"
val errorCode = TerminalException.TerminalErrorCode.READER_SOFTWARE_UPDATE_FAILED_READER_ERROR
val exception: TerminalException = mock {
on { errorMessage }.thenReturn(message)
on { this.errorCode }.thenReturn(errorCode)
}
whenever(terminalWrapper.connectToReader(any(), any(), any(), any())).thenAnswer {
(it.arguments[2] as ReaderCallback).onFailure(exception)
}

connectionManager.startConnectionToReader(cardReader, "location_id")

verify(terminalListenerImpl).updateReaderStatus(CardReaderStatus.NotConnected(message))
verify(terminalListenerImpl).updateReaderStatus(
CardReaderStatus.NotConnected(
errorCode = CardReaderStatus.NotConnected.ErrorCode.OTHER,
errorMessage = message,
)
)
}

@Test
fun `given reader with location id, when connectToReader fails with low batter, then status updated with not connected and batter error code`() =
testBlocking {
val reader: Reader = mock {
on { deviceType }.thenReturn(DeviceType.CHIPPER_2X)
}
val cardReader: CardReaderImpl = mock {
on { cardReader }.thenReturn(reader)
}
val message = "error_message"
val errorCode = TerminalException.TerminalErrorCode.READER_BATTERY_CRITICALLY_LOW
val exception: TerminalException = mock {
on { errorMessage }.thenReturn(message)
on { this.errorCode }.thenReturn(errorCode)
}
whenever(terminalWrapper.connectToReader(any(), any(), any(), any())).thenAnswer {
(it.arguments[2] as ReaderCallback).onFailure(exception)
}

connectionManager.startConnectionToReader(cardReader, "location_id")

verify(terminalListenerImpl).updateReaderStatus(
CardReaderStatus.NotConnected(
errorCode = CardReaderStatus.NotConnected.ErrorCode.BATTERY_CRITICALLY_LOW,
errorMessage = message,
)
)
}

@Test
Expand Down

0 comments on commit 1d8ab75

Please sign in to comment.