Skip to content

Commit c6e05e7

Browse files
fix(dashspend): add error handing for purchase limit mismatch (#1390)
* fix: add error handing for limit mismatch * fix: improve error checking on errorBody decoding Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * chore: ktlint * fix: fix limit error detection (min, max) * tests: add CTXSpendExceptionTest for limit errors * tests: add CTXSpendExceptionTest for malformed errorBody * style: ktlint --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 63da14f commit c6e05e7

File tree

8 files changed

+176
-9
lines changed

8 files changed

+176
-9
lines changed

common/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
<string name="log_in">Log In</string>
111111

112112
<string name="chain_syncing">The chain is syncing…</string>
113+
<string name="report_issue_dialog_mail_intent_chooser">Send report using…</string>
113114

114115
<!-- basic formats -->
115116
<string name="percent" translatable="false">%d%%</string>

features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
package org.dash.wallet.features.exploredash.repository
1919

20+
import com.google.gson.Gson
21+
import com.google.gson.reflect.TypeToken
2022
import kotlinx.coroutines.flow.Flow
2123
import org.dash.wallet.common.data.ResponseResource
2224
import org.dash.wallet.common.data.safeApiCall
@@ -31,12 +33,36 @@ import org.dash.wallet.features.exploredash.utils.CTXSpendConfig
3133
import java.util.UUID
3234
import javax.inject.Inject
3335

34-
class CTXSpendException(message: String) : Exception(message) {
36+
class CTXSpendException(
37+
message: String,
38+
val errorCode: Int? = null,
39+
val errorBody: String? = null
40+
) : Exception(message) {
3541
var resourceString: ResourceString? = null
42+
private val errorMap: Map<String, Any>
3643

3744
constructor(message: ResourceString) : this("") {
3845
this.resourceString = message
3946
}
47+
48+
init {
49+
val type = object : TypeToken<Map<String, Any>>() {}.type
50+
errorMap = try {
51+
if (errorBody != null) {
52+
Gson().fromJson(errorBody, type) ?: emptyMap()
53+
} else {
54+
emptyMap()
55+
}
56+
} catch (e: Exception) {
57+
emptyMap()
58+
}
59+
}
60+
61+
val isLimitError: Boolean
62+
get() {
63+
val fiatAmount = ((errorMap["fields"] as? Map<*, *>)?.get("fiatAmount") as? List<*>)?.firstOrNull()
64+
return errorCode == 400 && (fiatAmount == "above threshold" || fiatAmount == "below threshold")
65+
}
4066
}
4167

4268
class CTXSpendRepository @Inject constructor(

features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.dash.wallet.features.exploredash.ui.ctxspend
1919

20+
import android.content.Intent
2021
import androidx.lifecycle.*
2122
import androidx.lifecycle.LiveData
2223
import androidx.lifecycle.MutableLiveData
@@ -51,6 +52,7 @@ import org.dash.wallet.features.exploredash.data.explore.GiftCardDao
5152
import org.dash.wallet.features.exploredash.data.explore.model.Merchant
5253
import org.dash.wallet.features.exploredash.repository.CTXSpendException
5354
import org.dash.wallet.features.exploredash.repository.CTXSpendRepositoryInt
55+
import org.dash.wallet.features.exploredash.utils.CTXSpendConstants
5456
import org.slf4j.LoggerFactory
5557
import javax.inject.Inject
5658

@@ -137,8 +139,13 @@ class CTXSpendViewModel @Inject constructor(
137139
}
138140
is ResponseResource.Failure -> {
139141
log.error("purchaseGiftCard error ${response.errorCode}: ${response.errorBody}")
140-
throw CTXSpendException("purchaseGiftCard error ${response.errorCode}: ${response.errorBody}")
142+
throw CTXSpendException(
143+
"purchaseGiftCard error ${response.errorCode}: ${response.errorBody}",
144+
response.errorCode,
145+
response.errorBody
146+
)
141147
}
148+
// else -> {}
142149
}
143150
}
144151
throw CTXSpendException("purchaseGiftCard error")
@@ -272,4 +279,44 @@ class CTXSpendViewModel @Inject constructor(
272279
fun logEvent(eventName: String) {
273280
analytics.logEvent(eventName, mapOf())
274281
}
282+
283+
fun createEmailIntent(
284+
subject: String,
285+
ex: CTXSpendException
286+
) = Intent(Intent.ACTION_SEND).apply {
287+
setType("message/rfc822")
288+
putExtra(Intent.EXTRA_EMAIL, arrayOf(CTXSpendConstants.REPORT_EMAIL))
289+
putExtra(Intent.EXTRA_SUBJECT, subject)
290+
putExtra(Intent.EXTRA_TEXT, createReportEmail(ex))
291+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
292+
}
293+
294+
fun createReportEmail(ex: CTXSpendException): String {
295+
val report = StringBuilder()
296+
report.append("CTX Issue Report").append("\n")
297+
if (this::giftCardMerchant.isInitialized) {
298+
report.append("Merchant details").append("\n")
299+
.append("name: ").append(giftCardMerchant.name).append("\n")
300+
.append("id: ").append(giftCardMerchant.merchantId).append("\n")
301+
.append("min: ").append(giftCardMerchant.minCardPurchase).append("\n")
302+
.append("max: ").append(giftCardMerchant.maxCardPurchase).append("\n")
303+
.append("discount: ").append(giftCardMerchant.savingsFraction).append("\n")
304+
.append("denominations type: ").append(giftCardMerchant.denominationsType).append("\n")
305+
.append("denominations: ").append(giftCardMerchant.denominations).append("\n")
306+
.append("\n")
307+
} else {
308+
report.append("No merchant selected").append("\n")
309+
}
310+
report.append("\n")
311+
report.append("Purchase Details").append("\n")
312+
report.append("amount: ").append(giftCardPaymentValue.value.toFriendlyString()).append("\n")
313+
report.append("\n")
314+
ex.errorCode?.let {
315+
report.append("code: ").append(it).append("\n")
316+
}
317+
ex.errorBody?.let {
318+
report.append("body:\n").append(it).append("\n")
319+
}
320+
return report.toString()
321+
}
275322
}

features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
package org.dash.wallet.features.exploredash.ui.ctxspend.dialogs
1919

20+
import android.content.Intent
2021
import android.os.Bundle
2122
import android.view.View
23+
import androidx.activity.result.contract.ActivityResultContracts
2224
import androidx.annotation.StyleRes
2325
import androidx.constraintlayout.widget.ConstraintSet
2426
import androidx.core.view.isGone
@@ -64,6 +66,10 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi
6466
@Inject
6567
lateinit var authManager: AuthenticationManager
6668

69+
private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
70+
// Optionally handle result here
71+
}
72+
6773
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
6874
super.onViewCreated(view, savedInstanceState)
6975

@@ -101,12 +107,39 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi
101107
viewModel.purchaseGiftCard()
102108
} catch (ex: CTXSpendException) {
103109
hideLoading()
104-
AdaptiveDialog.create(
105-
R.drawable.ic_error,
106-
getString(R.string.gift_card_purchase_failed),
107-
ex.message ?: getString(R.string.gift_card_error),
108-
getString(R.string.button_close)
109-
).show(requireActivity())
110+
when {
111+
ex.errorCode == 400 && ex.isLimitError -> {
112+
AdaptiveDialog.create(
113+
R.drawable.ic_error,
114+
getString(R.string.gift_card_purchase_failed),
115+
getString(R.string.gift_card_limit_error),
116+
getString(R.string.button_close),
117+
getString(R.string.gift_card_contact_ctx)
118+
).show(requireActivity()) { result ->
119+
if (result == true) {
120+
// TODO: share
121+
val intent = viewModel.createEmailIntent(
122+
"CTX Issue: Spending Limit Problem",
123+
ex
124+
)
125+
126+
val chooser = Intent.createChooser(
127+
intent,
128+
getString(R.string.report_issue_dialog_mail_intent_chooser)
129+
)
130+
launcher.launch(chooser)
131+
}
132+
}
133+
}
134+
else -> {
135+
AdaptiveDialog.create(
136+
R.drawable.ic_error,
137+
getString(R.string.gift_card_purchase_failed),
138+
ex.message ?: getString(R.string.gift_card_error),
139+
getString(R.string.button_close)
140+
).show(requireActivity())
141+
}
142+
}
110143
return@launch
111144
}
112145

features/exploredash/src/main/java/org/dash/wallet/features/exploredash/utils/CTXSpendConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ object CTXSpendConstants {
2222
@JvmField var CLIENT_ID = "dcg_android"
2323
const val DEFAULT_DISCOUNT: Int = 0 // 0%
2424
const val DEFAULT_DISCOUNT_AS_DOUBLE: Double = 0.0 // 0%
25+
const val REPORT_EMAIL = "support@ctx.com"
2526
}

features/exploredash/src/main/res/values/strings-explore-dash.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@
228228
<string name="cancel">Cancel</string>
229229
<string name="gift_card_error">For some reason we were not able to buy a gift card. Please, try again.</string>
230230
<string name="gift_card_details_error">For some reason we were not able to buy a gift card. Please contact support.</string>
231+
<string name="gift_card_limit_error">The purchase limits for this merchant have changed. Please contact CTX Support for more information.</string>
231232
<string name="gift_card_unknown_error">For some reason we were not able to buy a gift card. Please contact support with the txid %s.</string>
232233
<string name="gift_card_rejected">The payment has been rejected. Please contact CTX with gift card id %s and payment id %s and transaction id %s.</string>
233234
<string name="gift_card_redeem_url_not_supported">The payment has was made for merchant that supplies a redeem URL, but that is not currently supported. Please contact CTX with gift card id %s and payment id %s and transaction id %s.</string>
@@ -237,6 +238,7 @@
237238
<string name="accept_to_proceed">To create an account at DashSpend you have to accept DashSpend terms and conditions</string>
238239
<string name="view_terms_conditions">Terms &amp; conditions</string>
239240
<string name="barcode_placeholder">As soon as your code is generated, it will be displayed here.</string>
241+
<string name="gift_card_contact_ctx">Contact CTX Support</string>
240242

241243
<!-- ctx spend login-->
242244
<string name="create_ctx_spend_account">Create account</string>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (c) 2025. Dash Core Group.
3+
* This program is free software: you can redistribute it and/or modify
4+
* it under the terms of the GNU General Public License as published by
5+
* the Free Software Foundation, either version 3 of the License, or
6+
* (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package org.dash.wallet.features.exploredash
18+
19+
import org.dash.wallet.features.exploredash.repository.CTXSpendException
20+
import org.junit.Assert.assertEquals
21+
import org.junit.Assert.assertFalse
22+
import org.junit.Assert.assertTrue
23+
import org.junit.Test
24+
25+
class CTXSpendExceptionTest {
26+
@Test
27+
fun limitErrorTest() {
28+
val errorBody = """
29+
{
30+
"fields": {
31+
"fiatAmount": [
32+
"above threshold"
33+
],
34+
"message": "Bad request"
35+
}
36+
}
37+
"""
38+
val exception = CTXSpendException("response error", 400, errorBody)
39+
assertEquals(400, exception.errorCode)
40+
assertTrue(exception.isLimitError)
41+
}
42+
43+
@Test
44+
fun unknownErrorTest() {
45+
val exception = CTXSpendException("other type of error")
46+
assertEquals(null, exception.errorCode)
47+
assertFalse(exception.isLimitError)
48+
}
49+
50+
@Test
51+
fun malformedJsonErrorTest() {
52+
val malformedErrorBody = "{ this is not valid json }"
53+
val exception = CTXSpendException("response error", 400, malformedErrorBody)
54+
assertEquals(400, exception.errorCode)
55+
// Verify that parsing errors don't cause the app to consider this a limit error
56+
assertFalse(exception.isLimitError)
57+
}
58+
}

wallet/res/values/strings.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@
295295
<string name="report_issue_dialog_collect_installed_packages">Append list of installed packages</string>
296296
<string name="report_issue_dialog_collect_application_log">Append application log</string>
297297
<string name="report_issue_dialog_collect_wallet_dump">Append wallet dump</string>
298-
<string name="report_issue_dialog_mail_intent_chooser">Send report using…</string>
299298
<string name="report_issue_dialog_mail_intent_failed">Sending report failed.</string>
300299
<string name="report_issue_dialog_generating_report">Generating report</string>
301300
<string name="report_transaction_history_export">Export</string>

0 commit comments

Comments
 (0)