Skip to content

feat(dashspend): support gift card quantities for piggycards#1483

Open
HashEngineering wants to merge 62 commits into
masterfrom
feat/piggycards-quantity
Open

feat(dashspend): support gift card quantities for piggycards#1483
HashEngineering wants to merge 62 commits into
masterfrom
feat/piggycards-quantity

Conversation

@HashEngineering
Copy link
Copy Markdown
Collaborator

@HashEngineering HashEngineering commented May 1, 2026

Issue being fixed or feature implemented

Related PR's and Dependencies

Screenshots / Videos

How Has This Been Tested?

  • QA (Mobile Team)

Checklist:

  • I have performed a self-review of my own code and added comments where necessary
  • I have added or updated relevant unit/integration/functional/e2e tests

Summary by CodeRabbit

  • New Features

    • Multi-card gift‑card shopping cart, new purchase screens, order & card-detail bottom sheets, and multi‑card order dialogs
    • New Enter Amount input with pinned numeric keypad, currency picker options, and reusable UI components (DashList, Grabber, TopIntroSend); new icons and light‑gray theme color
  • Refactor

    • Many dialogs/screens migrated from XML/ViewBinding to Compose; large-button styles consolidated to shared button component; new Compose bottom‑sheet base
  • Bug Fixes / Data

    • Database migration and DAO/repository updates to support multiple gift‑card entries
  • Documentation

    • Expanded Compose bottom‑sheet and subclassing guidance and usage rules

@HashEngineering HashEngineering self-assigned this May 1, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Composite gift-card support and a V2 PiggyCards purchase flow were added: DB schema and migration for (txId,index), DAOs/repositories/VMs updated for multi-card orders, new Compose UIs/dialogs and numeric entry components, resource/theme updates, test-merchant seeding, build flags, and documentation for EnterAmount and bottom-sheet patterns.

Changes

Gift card multi-card + PiggyCards V2

Layer / File(s) Summary
Data Shape / Entities
common/src/main/java/.../GiftCard.kt, features/exploredash/.../UpdatedMerchantDetails.kt, features/exploredash/.../Merchant.kt
GiftCard now uses composite primary keys ("txId","index") and adds index; UpdatedMerchantDetails adds quantity: Map<Double,Int>; Merchant adds ignored quantities and equals considers it.
Database Schema & Migration
wallet/schemas/.../AppDatabase/18.json, wallet/src/.../AppDatabase.kt, wallet/src/.../AppDatabaseMigrations.kt, wallet/src/.../DatabaseModule.kt
Room schema version bumped 17→18; migration 17→18 recreates gift_cards with non-null index, migrates existing rows to index=0, registers migration; schema snapshot added.
DAO / Persistence API
features/exploredash/.../GiftCardDao.kt, features/exploredash/.../GiftCardProviderDao.kt, features/exploredash/.../MerchantDao.kt
DAOs updated for multi-card semantics: single-card queries → lists/flows ordered by index; added bulk insertGiftCards; updateBarcode became index-aware; provider/merchant DAOs gained insert/delete-by-IDs.
Repository / Remote API Contract
features/exploredash/.../PiggyCardsRepository.kt, features/exploredash/.../DashSpendRepository.kt, features/exploredash/.../CTXSpendRepository.kt
Ordering APIs moved from single fiatAmount:String to order: GiftCardShoppingCart and now return List<GiftCardInfo>; getMerchant became denomination-aware (merchantId, type).
Service / Metadata Layer
common/.../TransactionMetadataProvider.kt, wallet/.../WalletTransactionMetadataProvider.kt, wallet/.../PlatformSyncService.kt
Metadata/barcode update signatures became index-aware (updateGiftCardBarcode(txId, index, ...)); upsert/cache writes constrained to index == 0 where appropriate; callers adjusted to use first card when single-card semantics were expected.
ViewModel & Business Logic
features/exploredash/.../DashSpendViewModel.kt, features/exploredash/.../GiftCardDetailsViewModel.kt, features/exploredash/.../GiftCardOrderDetailsViewModel.kt, features/exploredash/.../GiftCardViewModel.kt
ViewModels refactored for cart/multi-item orders: added GiftCardOrderItem/GiftCardShoppingCart, giftCardOrderInfo: StateFlow<Map<Double,Int>>; details VM now handles indexed giftCards/barcodes with index selector and refresh; new order-details VM and getGiftCardCount added.
UI / Compose Migration & New Screens
features/exploredash/.../PurchaseGiftCardFragmentV2.kt, features/exploredash/.../PurchaseGiftCardScreenV2.kt, features/exploredash/.../PurchaseGiftCardConfirmDialog.kt, features/exploredash/.../GiftCardDetailsDialog.kt, features/exploredash/.../GiftCardOrderDetailsDialog.kt
Several fragments/dialogs migrated to Compose ComposeBottomSheet; added V2 purchase fragment/screen supporting FlexibleSingle/FlexibleMultiple/Fixed modes, quantity steppers, cart/confirmation flows, and routing to order-details vs single-card details based on card count.
Numeric Entry & New Components
common/src/main/java/.../enter_amount/EnterAmountCompose.kt, common/src/main/java/.../EnterAmount.kt, common/src/main/java/.../DashList.kt, common/src/main/java/.../Grapper.kt, common/src/main/java/.../TopIntroSend.kt, common/src/main/java/.../TopNavBase.kt
Added EnterAmount composable (currency picker, locale symbol rules) and numeric keyboard (processAmountKeyInput/NumericKeyboardCompose); added DashList, Grabber, TopIntroSend, NavBarBackClose; theme color lightGray and new image vector added.
Dialog Base & Button Cleanup
common/src/main/java/.../ComposeBottomSheet.kt, common/src/main/java/.../OffsetDialogFragment.kt, common/.../ButtonLarge.kt (removed), common/.../ButtonStyle.kt (removed)
New ComposeBottomSheet fragment (auto-inserts Grabber, open @Composable fun Content() override); OffsetDialogFragment collapse-button access made type-tolerant; legacy ButtonLarge/ButtonStyles removed and callers migrated to DashButton.
Explore sync / Test merchant seeding
features/exploredash/.../ExploreSyncWorker.kt, features/exploredash/.../PiggyCardsTestMerchantData.kt, features/exploredash/.../PiggyCardsConstants.kt, features/exploredash/build.gradle, fastlane/Fastfile
Added PiggyCards test-merchant data and seeding invoked from explore sync worker; new build-time flag PIGGY_CARDS_TEST_MERCHANT controlled by BUILD_PROD (Fastfile sets BUILD_PROD=1 for prod).
Navigation & Wiring
features/exploredash/.../nav_explore.xml, features/exploredash/.../DashSpendUserAuthFragment.kt, features/exploredash/.../SearchFragment.kt, wallet/.../WalletTransactionsFragment.kt
Navigation graph adds purchaseGiftCardFragmentV2 and actions; auth/search flows route PiggyCards users to V2; transaction click handler now queries card count to choose order-details (multi) vs details (single).
Resources / Theming / Misc
common/.../res/drawable/*, features/exploredash/.../res/drawable/*, features/exploredash/.../res/values/strings-explore-dash.xml, common/res/values/strings.xml, common/.../MonetaryExt.kt, common/build.gradle, wallet/build.gradle
New drawables (nav info, dash D, gift icon, chevron, stepper icons), expanded strings for the purchase flow, Fiat.toDouble() added, Coil Compose dependency added, app version bumped, DB schema snapshot added.
Documentation / Patterns
.claude/agents/DEVELOPMENT-PATTERNS.md, .claude/agents/figma-to-compose.md
Docs updated with EnterAmount design guide, Compose bottom-sheet rules (backgroundStyle, Grabber behavior), required previews, subclass-pattern guidance, and figma→compose mapping including EnterAmount.
Previews / Tests (design-time)
numerous @Preview composables across added files
Many Compose previews added/updated for EnterAmount, numeric keyboard, purchase screens, confirm/details dialogs, and barcode/loading states.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as PurchaseGiftCardFragmentV2
    participant VM as DashSpendViewModel
    participant Repo as PiggyCardsRepository / CTX
    participant DB as Room (GiftCardDao)

    User->>UI: select merchant, amounts, quantities
    UI->>VM: setGiftCardOrderInfo(fiat, quantity)
    UI->>UI: render EnterAmount / DenominationList / NumericKeyboard
    UI->>VM: onContinue -> purchaseGiftCard()
    VM->>Repo: orderGiftcard(order: GiftCardShoppingCart)
    Repo->>Repo: remote API expands order -> List<GiftCardInfo>
    Repo->>VM: return List<GiftCardInfo>
    VM->>DB: saveGiftCardDummy(txId, List<GiftCardInfo>) (insert rows with index)
    VM->>UI: navigate -> GiftCardOrderDetailsDialog (if multiple) or GiftCardDetailsDialog (if single)
    UI->>DB: observeCardForTransaction(txId) -> UI updates with list/indices
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰
I nudged the schema, added an index light,
Composed a keyboard, made pickers bright,
PiggyCards line up, quantities in tow,
Dialogs hum in Compose’s gentle glow,
A hop, a patch, the gift-cart’s ready to go!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/piggycards-quantity

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/model/Merchant.kt (1)

80-92: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

equals() / hashCode() contract is broken after adding quantities.

Line 81 includes quantities in equality, but hashCode() does not. This can cause unstable behavior in hash-based structures.

Suggested fix
 override fun hashCode(): Int {
     var result = id.hashCode()
     result = 31 * result + (name?.hashCode() ?: 0)
     result = 31 * result + (active?.hashCode() ?: 0)
     result = 31 * result + denominations.hashCode()
     result = 31 * result + fixedDenomination.hashCode()
     result = 31 * result + (savingsPercentage?.hashCode() ?: 0)
     result = 31 * result + giftCardProviders.map { it.active }.hashCode()
+    result = 31 * result + quantities.hashCode()
     return result
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/model/Merchant.kt`
around lines 80 - 92, The equals()/hashCode() contract is broken because
equals() compares quantities but hashCode() does not; update the
Merchant.hashCode() implementation to include quantities (e.g., incorporate
quantities.hashCode() into the rolling result using the same 31 * result + ...
pattern) so it mirrors equals() which also uses giftCardProviders.map {
it.active } and quantities; ensure you update the method referenced as
hashCode() in Merchant.kt to keep parity with equals().
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt (1)

297-339: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the full cart total for limit, balance, and discount checks.

These paths still derive the amount from getFirstCardValueAsFiat(), so a quantity > 1 order can show a valid discount and no balance error even when the aggregated cart exceeds the purchase limits or available balance. Please base these checks/texts on the sum of giftCardOrderInfo, not the first line item.

Also applies to: 406-419

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt`
around lines 297 - 339, The code in PurchaseGiftCardFragment uses
viewModel.getFirstCardValueAsFiat() for limit, balance and discount logic which
misses quantity > 1 cases; update the checks and displayed amounts to use the
aggregated cart total (sum of giftCardOrderInfo) instead of the first line item:
replace calls to getFirstCardValueAsFiat() in the block that computes
savingsFraction, the zero-value check, exceedsBalance()/showBalanceError(),
viewModel.withinLimits(), and the purchaseAmount/discountedAmount used to
populate binding.discountValue.text with a cartTotal variable (calculated from
giftCardOrderInfo or a new viewModel method that returns the cart total as fiat)
so all validations and display use the full order total.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt (1)

248-289: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the selected provider before dereferencing it in the purchase path.

selectedProvider!! and provider!! turn missing restored state or a missing provider mapping into an NPE right when the user taps continue. This should fail with a domain error instead of crashing.

💡 Suggested fix
     suspend fun purchaseGiftCard(): List<GiftCardInfo> = withContext(Dispatchers.IO) {
         _giftCardMerchant.value?.merchantId?.let {
             ctxSpendConfig.set(CTXSpendConfig.PREFS_LAST_PURCHASE_START, System.currentTimeMillis())
+            val resolvedProvider = selectedProvider
+                ?: getSavedProvider()?.let(GiftCardProviderType::fromProviderName)
+                ?: throw CTXSpendException("purchaseGiftCard error: no giftcard provider")
+
             val giftCardOrderShoppingCart = GiftCardShoppingCart(
                 giftCardOrderInfo.value.map {
                     GiftCardOrderItem(
                         it.key,
                         it.value
                     )
                 }
             )
-            val provider = giftCardProviderDao.getProviderByMerchantId(it, selectedProvider!!.name)
-            when (selectedProvider) {
+            val provider = giftCardProviderDao.getProviderByMerchantId(it, resolvedProvider.name)
+                ?: throw CTXSpendException("purchaseGiftCard error: provider mapping missing")
+
+            when (resolvedProvider) {
                 GiftCardProviderType.CTX -> {
                     try {
                         ctxSpendRepository.orderGiftcard(
-                            merchantId = provider!!.sourceId,
+                            merchantId = provider.sourceId,
                             fiatCurrency = Constants.USD_CURRENCY,
                             order = giftCardOrderShoppingCart,
                             cryptoCurrency = Constants.DASH_CURRENCY
                         )
                     } catch (e: CTXSpendException) {
@@
                 GiftCardProviderType.PiggyCards -> {
                     try {
                         piggyCardsRepository.orderGiftcard(
                             cryptoCurrency = Constants.DASH_CURRENCY,
-                            merchantId = provider!!.sourceId,
+                            merchantId = provider.sourceId,
                             order = giftCardOrderShoppingCart,
                             fiatCurrency = Constants.USD_CURRENCY
                         )
                     } catch (e: CTXSpendException) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt`
around lines 248 - 289, The purchaseGiftCard flow dereferences selectedProvider
(!!) and provider (!!) which can cause NPEs; update purchaseGiftCard to validate
selectedProvider and the result of giftCardProviderDao.getProviderByMerchantId
before using them: if selectedProvider is null or provider is null return/throw
a domain-level error (e.g., a specific PurchaseValidationException or
CTXSpendException with a clear message) instead of proceeding, and use the
validated values in the existing branches (GiftCardProviderType.CTX /
PiggyCards) so the subsequent calls (ctxSpendRepository.orderGiftcard,
piggyCardsRepository.orderGiftcard) never operate on a null provider or
selectedProvider.
🟠 Major comments (19)
common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt-268-274 (1)

268-274: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Increase the eye icon tap target for accessibility.

Line 269 limits the interactive area to 20.dp, which is too small for reliable touch interaction.

Proposed fix
+import androidx.compose.foundation.layout.sizeIn
 ...
         Icon(
             painter = painterResource(
                 if (isVisible) R.drawable.ic_show else R.drawable.ic_hide
             ),
             contentDescription = if (isVisible) {
                 "Hide balance"
             } else {
                 "Show balance"
             },
             tint = MyTheme.Colors.textSecondary,
             modifier = Modifier
                 .size(20.dp)
+                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
                 .clickable(
                     interactionSource = interactionSource,
                     indication = null,
                     onClick = onToggleClick
                 )
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt`
around lines 268 - 274, The eye icon's Modifier currently sets a tiny
.size(20.dp) for the clickable area; update the Modifier where .size(20.dp) and
.clickable(... onToggleClick ...) are used in TopIntroSend.kt so the touch
target meets accessibility guidelines (>=48.dp). Replace or augment the modifier
with a minimum touch target helper such as .minimumTouchTargetSize() or use
.requiredSizeIn(minWidth = 48.dp, minHeight = 48.dp) (while keeping the visible
icon at 20.dp) so the clickable area around the eye (and its
interactionSource/onToggleClick) is enlarged without changing the visual icon
size.
common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt-100-113 (1)

100-113: ⚠️ Potential issue | 🟠 Major

Add an accessibility label for the delete button.

The delete key is interactive with both single-click and long-click actions, but currently exposes no readable label to screen readers, which makes correction flow impossible for assistive-tech users.

Suggested patch
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
...
                row.forEach { key ->
                    val isBack = key == "back"
+                    val a11yLabel = if (isBack) "Delete" else key
                     Box(
                         modifier = Modifier
                             .weight(1f)
                             .height(56.dp)
+                            .semantics { role = Role.Button }
                             .background(
                                 color = MyTheme.Colors.backgroundSecondary,
                                 shape = RoundedCornerShape(10.dp)
...
                         if (isBack) {
                             Icon(
                                 painter = painterResource(R.drawable.ic_delete_backward),
-                                contentDescription = null,
+                                contentDescription = a11yLabel,
                                 tint = MyTheme.Colors.textPrimary,
                                 modifier = Modifier.size(24.dp)
                             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt`
around lines 100 - 113, The delete key (the composable using combinedClickable
with onKeyInput and onLongClick when isBack is true, and the Icon with
painterResource(R.drawable.ic_delete_backward)) currently has contentDescription
= null so screen readers can't identify it; update this composable to provide an
accessible label (e.g., contentDescription = if (isBack) "Delete" else null or
use stringResource for localization) and ensure the semantics reflect both click
and long-click actions (use Modifier.semantics or combinedClickable with role =
Role.Button and add onLongClickLabel if available) so assistive tech can
announce the button and its long-press behavior while preserving the existing
onKeyInput/back_long handling.
common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt-111-114 (1)

111-114: ⚠️ Potential issue | 🟠 Major

Replace AppCompatImageButton check with ImageView to fix non-functional icon assignment.

The current type check against AppCompatImageButton never matches any actual layout declarations for collapse_button. Layout analysis reveals collapse_button is declared as ImageButton (majority), ImageView (4 layouts), or Button (2 layouts)—never AppCompatImageButton. This causes setImageResource() to never execute, leaving the icon unset in all scenarios.

Using ImageView instead correctly handles all cases: ImageButton (extends ImageView), standalone ImageView, and safely skips Button (which doesn't support setImageResource()).

Suggested fix
-import androidx.appcompat.widget.AppCompatImageButton
+import android.widget.ImageView
 ...
 view.findViewById<View?>(R.id.collapse_button)?.apply {
-    if (this is AppCompatImageButton) {
+    if (this is ImageView) {
         setImageResource(R.drawable.ic_popup_close_circle)
     }
     setOnClickListener { dismiss() }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt`
around lines 111 - 114, In OffsetDialogFragment replace the runtime type check
that currently tests for AppCompatImageButton on the view found via
view.findViewById(R.id.collapse_button) with a check against ImageView so
setImageResource(...) actually runs for ImageButton and ImageView variants;
specifically, locate the code block in OffsetDialogFragment where
collapse_button is retrieved and change the conditional type test to ImageView,
then call setImageResource(R.drawable.ic_popup_close_circle) on that ImageView
instance so Buttons are skipped and image-bearing views receive the icon.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/dashspend/model/UpdatedMerchantDetails.kt-30-30 (1)

30-30: ⚠️ Potential issue | 🟠 Major

Avoid Double as the denomination key for quantity mapping.

At Line 30, Map<Double, Int> creates fragile monetary identity logic. The code directly uses Double denomination values from the denominations list to access this map (seen in PurchaseGiftCardFragmentV2.kt and elsewhere), but floating-point precision mismatches will break key equality and return wrong/missing inventory counts.

💡 Safer modeling direction
-data class UpdatedMerchantDetails(
+data class UpdatedMerchantDetails(
     val id: String,
     val denominations: List<Double>,
@@
-    val quantity: Map<Double, Int> = mapOf()
+    // key in minor units (e.g., cents) to avoid floating-point key mismatch
+    val quantityByMinorUnit: Map<Long, Int> = emptyMap()
 )

Convert all denominations to cents (or smallest unit) before map operations. This ensures bit-exact integer matching instead of floating-point approximation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/dashspend/model/UpdatedMerchantDetails.kt`
at line 30, UpdatedMerchantDetails.quantity uses Map<Double, Int>, which causes
fragile lookup due to floating-point precision; change the model and all usages
to use integer smallest-unit keys (e.g., cents) instead of Double: update the
UpdatedMerchantDetails data class field name/type from quantity: Map<Double,
Int> to an integer-keyed map (e.g., Map<Long, Int> or Map<Int, Int> representing
cents), convert the denominations list values to cents when building or
accessing this map, and update all callsites (notably
PurchaseGiftCardFragmentV2, any code that reads denominations and indexes
quantity) to convert denomination double values to the integer cents key before
map lookup and when populating the map to ensure exact equality and avoid
floating-point mismatches.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ExploreSyncWorker.kt-173-239 (1)

173-239: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor the build flag before touching test-merchant rows.

addPiggyCardsTestMerchantIfNeeded() currently rewrites PiggyCards test data unconditionally because the SUPPORT_PIGGY_CARDS_TEST_MERCHANT gate is commented out. That means production builds still delete/reseed test merchants, and the getMerchantById() check below is dead code because those rows were just removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ExploreSyncWorker.kt`
around lines 173 - 239, The method addPiggyCardsTestMerchantIfNeeded currently
always deletes and re-inserts test merchants because the
SUPPORT_PIGGY_CARDS_TEST_MERCHANT gate is commented out; restore and honor that
build flag by wrapping the deletion/insert logic (the calls to
giftCardProviderDao.deleteByMerchantIds, merchantDao.deleteByMerchantIds, the
getMerchantById check, merchantDao.save and giftCardProviderDao.insert loops)
inside an if (SUPPORT_PIGGY_CARDS_TEST_MERCHANT) { ... } block (or return early
when the flag is false) so production builds skip touching
PiggyCardsTestMerchants and the PIGGY_CARDS_TEST_FIXED_MERCHANT_ID existence
check remains meaningful.
wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt-1244-1247 (1)

1244-1247: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't call first() on an optional gift-card list here.

publishPastTxMetadata() walks every transaction that has metadata, not just gift-card purchases. If getCardForTransaction(tx.txId) returns an empty list, this throws and aborts the whole backfill. The same file already treats gift cards as optional in getUnsavedTransactions() via firstOrNull(), so this path needs the same guard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt`
around lines 1244 - 1247, publishPastTxMetadata() currently assumes
getCardForTransaction(tx.txId) returns a non-empty list and calls
giftCard.first(), which throws when the list is empty and aborts the backfill;
change this to treat the gift card as optional (like getUnsavedTransactions()
uses firstOrNull()), e.g. obtain the gift card with firstOrNull() and only
construct TransactionMetadataCacheItem when that gift card is non-null (or
handle the null case appropriately), ensuring TransactionMetadataCacheItem(...)
is not called with a missing gift card.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendUserAuthFragment.kt-243-246 (1)

243-246: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Route off the verified provider, not viewModel.selectedProvider.

This can turn a successful OTP verification into the generic invalid-code error if selectedProvider is null/stale after recreation. verifyEmail(provider, code) already proved which provider this flow belongs to, so this branch should use that argument directly and stay exhaustive.

Suggested fix
-                    when (viewModel.selectedProvider) {
+                    when (provider) {
                         GiftCardProviderType.CTX -> safeNavigate(DashSpendUserAuthFragmentDirections.authToPurchaseGiftCardFragment())
                         GiftCardProviderType.PiggyCards -> safeNavigate(DashSpendUserAuthFragmentDirections.authToPurchaseGiftCardFragmentV2())
-                        else -> error("serious error. provider = null")
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendUserAuthFragment.kt`
around lines 243 - 246, The navigation branch is using
viewModel.selectedProvider which can be null/stale; instead use the verified
provider argument passed into verifyEmail(provider, code) to decide the route.
Update the when expression to switch on the local provider parameter (the same
one passed into verifyEmail) and handle GiftCardProviderType.CTX ->
safeNavigate(DashSpendUserAuthFragmentDirections.authToPurchaseGiftCardFragment()),
GiftCardProviderType.PiggyCards ->
safeNavigate(DashSpendUserAuthFragmentDirections.authToPurchaseGiftCardFragmentV2()),
and an exhaustive else that throws an error if an unknown provider appears;
replace references to viewModel.selectedProvider with the provider parameter to
avoid stale-null issues.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt-398-402 (1)

398-402: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle empty results and missing DASH payment URLs before dereferencing.

data.first() plus paymentUrls?.get("DASH.DASH")!! will crash on an unexpected provider response instead of surfacing a purchase failure. Please guard both cases and route them into the existing error handling path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 398 - 402, Guard against empty or malformed provider responses
before dereferencing: check that the incoming data list is not empty and that
the first element contains a non-null DASH payment URL
(paymentUrls?.get("DASH.DASH") != null) before calling
createSendingRequestFromDashUri; if either check fails, route to the existing
error path (the same failure handling used elsewhere in this dialog) instead of
proceeding to call enterAmountViewModel.clearSavedState(),
viewModel.saveGiftCardDummy(transactionId, data) and
showGiftCardDetailsDialog(transactionId, data.first().id); update the block
around createSendingRequestFromDashUri and the subsequent let { … } to perform
these guards and call the dialog’s error handler when guards fail.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt-357-360 (1)

357-360: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop polling and surface an error when the PiggyCards order id is missing.

This branch only logs and returns, so the ticker keeps firing and the UI never transitions to a terminal error state. Cancel the ticker and publish an error here instead of retrying forever.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt`
around lines 357 - 360, In GiftCardDetailsViewModel.kt, modify the branch that
handles a null orderId (where you call
giftCardDao.getCardForTransaction(txid).firstOrNull()?.note and currently only
log.error) so that you cancel the active polling ticker (call ticker.cancel() or
the cancellation method used where the ticker is created) and publish a terminal
error to the UI instead of silently returning (emit/update the ViewModel's
error/state flow or LiveData used by this class, e.g., the _viewState/_error
LiveData or send a UiEvent) so the UI transitions to an error state and polling
stops.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt-488-497 (1)

488-497: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Choose the details dialog by purchased card count, not map-entry count.

giftCardOrderInfo.value.values.size is the number of distinct denominations, not the number of cards. A 3 x $20 purchase still has size 1, so this opens the single-card dialog for a multi-card order. data.size would be the safest discriminator here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 488 - 497, The condition in showGiftCardDetailsDialog currently
uses giftCardOrderInfo.value.values.size (count of distinct denominations) to
choose between GiftCardOrderDetailsDialog and GiftCardDetailsDialog; change it
to use the actual number of purchased cards instead (e.g. compute totalCards =
giftCardOrderInfo.value.values.sumOf { it.size } or use
giftCardOrderInfo.value.data.size if a flat list exists) and branch on
totalCards > 1 so multi-card purchases open GiftCardOrderDetailsDialog and
single-card purchases open GiftCardDetailsDialog.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt-166-170 (1)

166-170: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't silently drop extra cart lines in orderGiftcard().

The new signature accepts a shopping cart, but only order.first() is used. If a multi-entry cart reaches this repository, the request amount no longer matches what the caller thinks it submitted. Either reject order.size != 1 here or translate every cart line explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt`
around lines 166 - 170, The orderGiftcard method in CTXSpendRepository currently
reads only order.first() from the GiftCardShoppingCart and silently ignores
additional lines; update orderGiftcard (and its call to purchaseGiftCard) to
either validate and reject multi-line carts (throw IllegalArgumentException or
return an error when order.size != 1) or iterate the GiftCardShoppingCart and
aggregate/translate every cart line into the appropriate purchase requests
(e.g., map each entry to a fiatAmount/merchantId pair and call purchaseGiftCard
for each or extend purchaseGiftCard to accept multiple lines). Ensure you
reference the GiftCardShoppingCart parameter, the orderGiftcard function, and
the purchaseGiftCard call when making the change so callers’ submitted amounts
are preserved or rejected explicitly.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt-147-149 (1)

147-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Compute the confirm-sheet discount per cart line, not from the aggregated total.

getGiftCardDiscount() is denomination-based elsewhere in this flow. Passing the summed order amount here can misstate discountText and youPayText for fixed-denomination quantity purchases or mixed-denomination carts. Sum the discounted value per (denomination, qty) instead of applying one fraction to the whole order.

Also applies to: 173-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 147 - 149, Instead of deriving a single savingsFraction from the
aggregated orderTotalAmount, compute discounts per cart line: iterate
viewModel.giftCardOrderInfo.entries and for each (denomination, qty) call
viewModel.getGiftCardDiscount(denomination.toBigDecimal().toDouble()) to get the
per-denomination fraction, multiply by denomination * qty to accumulate
totalSavings, then compute youPay = orderTotalAmount - totalSavings and build
discountText/youPayText from totalSavings and youPay; apply this change where
orderTotalAmount/savingsFraction are currently computed (the block using
getGiftCardDiscount and the similar logic around lines 173-188) and replace any
single-fraction usage with the per-line aggregated savings.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt-125-140 (1)

125-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rebuild barcodes from the latest cards instead of keeping the first snapshot forever.

After the first emission, this branch keeps reusing currentState.barcodes. Later updateGiftCardBarcode() writes can update the DAO rows without ever refreshing uiState.barcodes, and newly added card slots won't get a corresponding barcode entry. Re-derive or merge the barcode list on every giftCards emission.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt`
around lines 125 - 140, The current .onEach block in the
GiftCardDetailsViewModel updates _uiState but only initializes barcodes once
(when currentState.barcodes.isEmpty()), causing subsequent giftCards emissions
to reuse stale barcode slots; change the update in the .onEach that handles
giftCards so barcodes are re-derived or merged on every emission: map the
incoming giftCards to Barcode? by checking each giftCard's barcodeValue and
barcodeFormat and produce Barcode(value, format) or null, and if you need to
preserve user/DAO-updated barcode entries merge them by matching the same
identifier (e.g., card id or stable index) against currentState.barcodes so
preserved non-null entries are kept, then assign that resulting list to the
barcodes field in the _uiState.update (in the same block that currently sets
giftCards and index).
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt-144-150 (1)

144-150: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't start a new ticker if one is already active.

This observer launches a fresh TickerFlow on every qualifying DAO emission and overwrites tickerJob without cancelling the previous one. A pending order that updates a few times can end up polling the provider multiple times in parallel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt`
around lines 144 - 150, The current observer can start multiple TickerFlow
instances because it always assigns a new tickerJob; modify the block so it
first checks tickerJob?.isActive (or tickerJob != null && tickerJob.isActive)
and only create/launch the TickerFlow when no active job exists (i.e., skip
creating a new one if tickerJob is active). Use the existing symbols: tickerJob,
TickerFlow, fetchGiftCardInfo(transactionId) and viewModelScope to locate the
code and gate creation accordingly; alternatively, if you prefer restarting
behavior, call tickerJob?.cancel() before assigning a new TickerFlow.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt-151-158 (1)

151-158: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce allowedQuantities in the stepper.

PurchaseGiftCardV2UiState.allowedQuantities never reaches DenominationList(), so the + button can keep incrementing past the provider's advertised inventory. That makes invalid carts easy to construct and pushes a preventable failure to the backend. Pass the per-denomination cap into the list/row and disable increment at that limit.

Also applies to: 160-166, 393-429

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt`
around lines 151 - 158, The per-denomination cap from
PurchaseGiftCardV2UiState.allowedQuantities is never threaded into the UI, so
the stepper can increment beyond available inventory; update the call sites
(e.g., when handling GiftCardPurchaseMode.FlexibleMultiple in
PurchaseGiftCardScreenV2 and the other similar blocks at 160-166 and 393-429) to
pass the matching allowedQuantities list into FlexibleMultipleContent (and any
downstream composables like DenominationList/DenominationRow or the stepper
component), then update the stepper logic to disable the "+" action (and prevent
onQuantityChanged from increasing) when the current quantity >=
allowedQuantities[index] for that denomination. Ensure indices align between
denominations and allowedQuantities and propagate the allowed cap through the
same props the current quantity uses.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt-134-181 (1)

134-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align the balance check with the insufficient-funds error state.

canContinue rejects amount == balanceMax / totalDouble == balanceMax because it uses <, but the error branch only reports insufficient funds for > balanceMax. At the exact balance, the CTA is disabled with no explanation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`
around lines 134 - 181, The CTA is disabled at exactly-equal balance because
canContinue uses strict '<' against balanceMax while errorText only treats '>'
as insufficient funds; update both the eligibility and error checks to treat
equality consistently. In the GiftCardPurchaseFragmentV2 logic, change the
FlexibleSingle canContinue check (variable canContinue / branch
GiftCardPurchaseMode.FlexibleSingle) from amount < balanceMax to amount <=
balanceMax and adjust the matching errorText branch to mark amount >= balanceMax
as insufficient funds; likewise update the FlexibleMultiple/Fixed branch (where
totalDouble is compared to fiatBalance.toBigDecimal().toDouble()) from
totalDouble < balanceMax to totalDouble <= balanceMax and make the error branch
use totalDouble >= balanceMax so CTA state and error messaging align.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt-190-197 (1)

190-197: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Keep the new ViewModel state immutable to callers.

These new properties are public MutableStateFlows, and the fragment already mutates them directly. That makes it easy to bypass purchase/cart invariants as this flow grows; keep the mutable backing state private and expose immutable StateFlows instead.

As per coding guidelines, "Use private mutable _uiState with public immutable uiState via asStateFlow() in ViewModels".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt`
around lines 190 - 197, Make the mutable flows private and expose immutable
StateFlow views: rename the public MutableStateFlow symbols to private backing
properties (e.g., _isFixedDenominationMultiple: MutableStateFlow<Boolean?> and
_giftCardOrderInfo: MutableStateFlow<Map<Double,Int>>) and add public read-only
properties isFixedDenominationMultiple: StateFlow<Boolean?> =
_isFixedDenominationMultiple.asStateFlow() and giftCardOrderInfo:
StateFlow<Map<Double,Int>> = _giftCardOrderInfo.asStateFlow(); update any
callers to mutate only the private backing properties (or via ViewModel methods)
instead of directly modifying the previously-public MutableStateFlow instances.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt-381-387 (1)

381-387: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the option-card productId to every cart item.

In the option-card path, only thisOrder.first() gets a productId. Any additional cart entries keep 0, so the order payload becomes invalid as soon as the cart contains more than one item.

💡 Suggested fix
             } else if (optionGiftcard != null) {
-                // there is probably a bug here, but there are no option cards to test
-                thisOrder.first().productId = optionGiftcard.id
+                thisOrder.forEach { orderItem ->
+                    orderItem.productId = optionGiftcard.id
+                }
             } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt`
around lines 381 - 387, The option-card branch only sets productId on
thisOrder.first() leaving other OrderItem.productId values unset; change the
else-if handling so that when optionGiftcard != null you iterate over thisOrder
(same as the rangeGiftCard branch) and assign orderItem.productId =
optionGiftcard.id for each item (use the existing thisOrder.forEach lambda and
optionGiftcard.id) so every cart item gets the correct productId.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt-41-55 (1)

41-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Order multi-card queries by index.

These queries now return multiple rows per txId, but they don't specify ORDER BY \index`. The rest of this flow uses positional cardIndex`, so an unstable row order can show or update the wrong card/barcode for a transaction.

💡 Suggested fix
-    `@Query`("SELECT * FROM gift_cards WHERE txId = :txId")
+    `@Query`("SELECT * FROM gift_cards WHERE txId = :txId ORDER BY `index` ASC")
     suspend fun getCardForTransaction(txId: Sha256Hash): List<GiftCard>

-    `@Query`("SELECT * FROM gift_cards WHERE txId = :txId")
+    `@Query`("SELECT * FROM gift_cards WHERE txId = :txId ORDER BY `index` ASC")
     fun observeCardForTransaction(txId: Sha256Hash): Flow<List<GiftCard>>

     `@MapInfo`(keyColumn = "txId")
-    `@Query`("SELECT * FROM gift_cards")
+    `@Query`("SELECT * FROM gift_cards ORDER BY txId, `index` ASC")
     fun observeGiftCards(): Flow<Map<Sha256Hash, List<GiftCard>>>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt`
around lines 41 - 55, Queries that return multiple GiftCard rows must have a
stable order by index to ensure positional cardIndex matches rows; update the
`@Query` for getCardForTransaction and observeCardForTransaction to include "ORDER
BY `index`" so the list is ordered by card index, and update the `@Query` used by
observeGiftCards (with `@MapInfo` keyColumn = "txId") to order by txId and `index`
(e.g. "ORDER BY txId, `index`") so each Map value List<GiftCard> preserves
correct per-transaction ordering.
🟡 Minor comments (6)
common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt-262-266 (1)

262-266: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize accessibility labels instead of hardcoded English.

Lines 262-266 use hardcoded content descriptions ("Hide balance" / "Show balance"), which won’t be translated.

Proposed fix
             contentDescription = if (isVisible) {
-                "Hide balance"
+                stringResource(R.string.hide_balance)
             } else {
-                "Show balance"
+                stringResource(R.string.show_balance)
             },

Also add resources:

<string name="hide_balance">Hide balance</string>
<string name="show_balance">Show balance</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt`
around lines 262 - 266, The contentDescription in TopIntroSend.kt is using
hardcoded English ("Hide balance"/"Show balance") which prevents localization;
update the conditional to use string resources via
stringResource(R.string.hide_balance) and stringResource(R.string.show_balance)
for the contentDescription (locate the contentDescription property in the
TopIntroSend composable), and add corresponding entries in strings.xml (<string
name="hide_balance">Hide balance</string> and <string name="show_balance">Show
balance</string>) so the labels can be translated.
.claude/agents/DEVELOPMENT-PATTERNS.md-1326-1326 (1)

1326-1326: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced code block.

Line [1326] uses a plain triple-backtick fence; markdownlint MD040 expects a language tag.

🧩 Suggested lint fix
-```
+```kotlin
 `@AndroidEntryPoint`
 class FeatureDetailsDialog : ComposeBottomSheet() {   // Layer 1: lifecycle + plumbing
     override fun Content() { FeatureDetailsContent(...) } // bridges to layer 2
 }
@@
-```
+```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/agents/DEVELOPMENT-PATTERNS.md at line 1326, The fenced code block
containing the Kotlin snippet for the FeatureDetailsDialog class (references:
FeatureDetailsDialog, ComposeBottomSheet, Content, FeatureDetailsContent) is
missing a language identifier; update the opening triple-backtick fence to
include "kotlin" (i.e., replace ``` with ```kotlin) so markdownlint MD040 is
satisfied and the code block is properly highlighted.
features/exploredash/src/main/res/values/strings-explore-dash.xml-229-229 (1)

229-229: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a plural resource for inventory errors instead of hardcoded “cards”.

Line 229 always renders “cards”, so quantity 1 will be grammatically wrong and harder to localize.

💡 Proposed i18n-safe change
-    <string name="purchase_gift_card_insufficient_inventory">Insufficient inventory for %s to buy %d x %s cards</string>
+    <plurals name="purchase_gift_card_insufficient_inventory">
+        <item quantity="one">Insufficient inventory for %1$s to buy %2$d x %3$s card</item>
+        <item quantity="other">Insufficient inventory for %1$s to buy %2$d x %3$s cards</item>
+    </plurals>

Then resolve with resources.getQuantityString(..., quantity, merchant, quantity, denomination).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/exploredash/src/main/res/values/strings-explore-dash.xml` at line
229, Replace the hardcoded singular/plural string
"purchase_gift_card_insufficient_inventory" with an Android plurals resource
(e.g., name it purchase_gift_card_insufficient_inventory) and update the call
site to use resources.getQuantityString(pluralsId, quantity, merchant, quantity,
denomination) so the message correctly pluralizes "card(s)" based on the
quantity; locate usages referencing the string resource name
purchase_gift_card_insufficient_inventory and change them to request the plurals
resource with the quantity and the existing parameters (merchant, quantity,
denomination).
common/src/main/java/org/dash/wallet/common/ui/components/TopNavBase.kt-228-244 (1)

228-244: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix incorrect KDoc: function has both back and close, not "back chevron only".

The documentation was copied from NavBarBack and doesn't match the actual functionality.

📝 Proposed fix
-/** NavBarBack — back chevron only, no title. */
+/** NavBarBackClose — back chevron + close button, no title. */
 `@Composable`
 fun NavBarBackClose(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/src/main/java/org/dash/wallet/common/ui/components/TopNavBase.kt`
around lines 228 - 244, Update the KDoc above the NavBarBackClose composable to
accurately describe that this function provides both a back chevron (leading)
and a close button (trailing), instead of the current "back chevron only" text;
locate the comment for the NavBarBackClose function and replace the copied
NavBarBack description with a concise summary mentioning both back and close
actions and their callbacks (onBackClick, onCloseClick).
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt-222-226 (1)

222-226: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the logged retry count in sync with WAIT_LIMIT_FOR_ERROR.

WAIT_LIMIT_FOR_ERROR is 60, but this message says "after 10 tries". That makes the support/analytics breadcrumb inaccurate on the only path where it matters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt`
around lines 222 - 226, The error breadcrumb message in the LaunchedEffect block
(inside GiftCardDetailsDialog.kt) is hardcoding "after 10 tries" while the
actual threshold is WAIT_LIMIT_FOR_ERROR / waitLimitForError; update the
onErrorLogged call inside the LaunchedEffect (the block that checks
uiState.error and uiState.queries == waitLimitForError) to interpolate the
actual constant/variable (WAIT_LIMIT_FOR_ERROR or waitLimitForError) instead of
the literal 10 so the logged message for uiState.giftCard?.merchantName reflects
the real retry count.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt-177-190 (1)

177-190: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Build the inventory error from the denomination that actually exceeds stock.

You already compute denomsExceedInventory, but the message is formatted from denominationQuantities.entries.first(). If the first selected denomination is still in stock, the user gets the wrong amount/quantity in the error.

💡 Suggested fix
-                        val firstCard = denominationQuantities.entries.first()
+                        val firstCard = denomsExceedInventory.first()
                         getString(
                             R.string.purchase_gift_card_insufficient_inventory,
                             merchant?.name,
                             firstCard.value,
                             Fiat.parseFiat("$", firstCard.key.toString()).toFormattedString()
                         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`
around lines 177 - 190, The inventory error uses
denominationQuantities.entries.first() instead of the actual out-of-stock entry;
change the code to select the first element from denomsExceedInventory (e.g.,
val firstCard = denomsExceedInventory.first()) and then format the message using
that entry's key/ value and merchant?.name so the error shows the denomination
and quantity that truly exceed inventory (refer to denomsExceedInventory,
denominationQuantities, and firstCard in PurchaseGiftCardFragmentV2).
🧹 Nitpick comments (3)
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/DashSpendRepository.kt (1)

23-43: 🏗️ Heavy lift

Avoid repository dependency on a UI-layer model.

Line [23] imports GiftCardShoppingCart from ui.dashspend, which couples the repository contract to UI types. Please move this cart model (or a dedicated order DTO) into a domain/data package and have UI map into it.

🏗️ Directional boundary fix (example)
-import org.dash.wallet.features.exploredash.ui.dashspend.GiftCardShoppingCart
+import org.dash.wallet.features.exploredash.data.dashspend.model.GiftCardOrder

 interface DashSpendRepository {
@@
-    suspend fun orderGiftcard(
+    suspend fun orderGiftcard(
         cryptoCurrency: String,
         fiatCurrency: String,
-        order: GiftCardShoppingCart,
+        order: GiftCardOrder,
         merchantId: String,
     ): List<GiftCardInfo>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/DashSpendRepository.kt`
around lines 23 - 43, DashSpendRepository currently depends on a UI model
(GiftCardShoppingCart imported from ui.dashspend) which violates layer
boundaries; move or recreate a domain-level order DTO (e.g., GiftCardOrder or
GiftCardRequest) into a domain/data package and update the
DashSpendRepository.orderGiftcard signature to accept that DTO instead of
GiftCardShoppingCart, then update UI code to map its GiftCardShoppingCart to the
new DTO before calling signup/login/ orderGiftcard; ensure all references to
GiftCardShoppingCart in DashSpendRepository and related repository
implementations are replaced with the new domain DTO.
common/src/main/java/org/dash/wallet/common/ui/components/Grapper.kt (1)

54-58: ⚡ Quick win

Fix naming typo to match the component name.

On Line [56], GrapperPreview should be GrabberPreview. Also consider renaming Grapper.kt to Grabber.kt so file and symbol names align.

✏️ Suggested rename in this file
-@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
-@Composable
-private fun GrapperPreview() {
+@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
+@Composable
+private fun GrabberPreview() {
     Grabber()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/src/main/java/org/dash/wallet/common/ui/components/Grapper.kt` around
lines 54 - 58, Rename the preview function GrapperPreview to GrabberPreview and
update any references so it calls the existing composable Grabber(), and also
rename the file Grapper.kt to Grabber.kt to keep filename and symbol names
consistent; ensure the Kotlin file name and the `@Composable` function name match
(GrabberPreview) and update any imports/usages that reference GrapperPreview or
Grapper.kt.
common/src/main/java/org/dash/wallet/common/data/entity/GiftCard.kt (1)

21-21: 💤 Low value

Unused import: PrimaryKey is no longer used.

The primary key is now defined in the @Entity annotation. This import can be removed.

🧹 Proposed fix
 import androidx.room.Entity
-import androidx.room.PrimaryKey
 import com.google.zxing.BarcodeFormat
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@common/src/main/java/org/dash/wallet/common/data/entity/GiftCard.kt` at line
21, The import androidx.room.PrimaryKey is unused in the GiftCard class because
the primary key is declared in the `@Entity` annotation; remove the unused import
statement to clean up the file (look for the import line referencing PrimaryKey
in the GiftCard.kt file and delete it).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ffa3d319-cf48-442d-a712-7c193c2877ef

📥 Commits

Reviewing files that changed from the base of the PR and between 58b2e97 and 8f64385.

📒 Files selected for processing (62)
  • .claude/agents/DEVELOPMENT-PATTERNS.md
  • common/build.gradle
  • common/src/main/java/org/dash/wallet/common/data/entity/GiftCard.kt
  • common/src/main/java/org/dash/wallet/common/services/TransactionMetadataProvider.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/ButtonLarge.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/ButtonStyle.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/DashList.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/Grapper.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/ListItem.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/MyImages.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/MyTheme.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/TopNavBase.kt
  • common/src/main/java/org/dash/wallet/common/ui/dialogs/ComposeBottomSheet.kt
  • common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt
  • common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt
  • common/src/main/java/org/dash/wallet/common/util/MonetaryExt.kt
  • common/src/main/res/drawable/ic_dash_d_gray.xml
  • common/src/main/res/drawable/ic_nav_bar_info.xml
  • common/src/main/res/layout/dialog_compose_sheet.xml
  • common/src/main/res/values/strings.xml
  • fastlane/Fastfile
  • features/exploredash/build.gradle
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ExploreSyncWorker.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/dashspend/GiftCardProviderDao.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/dashspend/model/UpdatedMerchantDetails.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/MerchantDao.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/model/Merchant.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/DashSpendRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendUserAuthFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/MerchantDenominations.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/explore/SearchFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/utils/PiggyCardsConstants.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/utils/PiggyCardsTestMerchantData.kt
  • features/exploredash/src/main/res/drawable/ic_gift_card_icon.xml
  • features/exploredash/src/main/res/layout/dialog_confirm_purchase_gift_card.xml
  • features/exploredash/src/main/res/layout/dialog_gift_card_details.xml
  • features/exploredash/src/main/res/navigation/nav_explore.xml
  • features/exploredash/src/main/res/values/strings-explore-dash.xml
  • wallet/build.gradle
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/18.json
  • wallet/src/de/schildbach/wallet/database/AppDatabase.kt
  • wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt
  • wallet/src/de/schildbach/wallet/di/DatabaseModule.kt
  • wallet/src/de/schildbach/wallet/service/WalletTransactionMetadataProvider.kt
  • wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt
  • wallet/src/de/schildbach/wallet/ui/OnboardingActivity.kt
  • wallet/src/de/schildbach/wallet/ui/TransactionResultViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt
💤 Files with no reviewable changes (3)
  • features/exploredash/src/main/res/layout/dialog_confirm_purchase_gift_card.xml
  • common/src/main/java/org/dash/wallet/common/ui/components/ButtonLarge.kt
  • common/src/main/java/org/dash/wallet/common/ui/components/ButtonStyle.kt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (5)
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt (1)

248-335: 🏗️ Heavy lift

Heavy duplication with the existing getMerchant(merchantId).

This new overload re-implements ~80 lines that already exist in the original getMerchant(merchantId) at lines 156–246 (brand lookup, disabled-card filtering, immediate/non-immediate split, option/range card handling, fallback UpdatedMerchantDetails). The only material difference is the priority tree being pruned per DenominationType. Any future fix (e.g., disabled-merchant rules, discount math, quantity map) now has to be made in two places and will drift.

Consider consolidating: extract the brand+gift-card resolution and each card-shape branch (fixed / option / range / empty) into private helpers, and have both public entry points call them with a flag/type. At minimum, have the original getMerchant(merchantId) delegate to this overload (e.g., trying Fixed then MinMax) so the empty / disabled / discount logic lives in one spot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt`
around lines 248 - 335, The two getMerchant overloads duplicate brand lookup,
disabled-card filtering and card-shape handling; refactor by extracting helpers
such as resolveBrandAndGiftCards(merchantId): Pair<Brand, List<GiftCard>> and
handlers buildFixedMerchantDetails(...), buildOptionMerchantDetails(...),
buildRangeMerchantDetails(... ) that encapsulate the common logic (use symbols
giftCardMap, disabledGiftCards, disabledMerchants, SERVICE_FEE,
UpdatedMerchantDetails), then have getMerchant(merchantId, type:
DenominationType) call those helpers according to DenominationType.Fixed /
MinMax and make the original getMerchant(merchantId) delegate to the overload
(e.g., try Fixed then MinMax) so all filtering, discount math and
enabled/productId logic live in one place.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt (2)

491-507: 💤 Low value

giftCardId parameter is unused.

showGiftCardDetailsDialog accepts giftCardId: String but never reads it (both branches only use txId). Either drop the parameter from the signature and the call site on Line 405, or pass it on to GiftCardOrderDetailsDialog.newInstance / GiftCardDetailsDialog.newInstance if the index-aware flow needs it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 491 - 507, The function showGiftCardDetailsDialog(txId: Sha256Hash,
giftCardId: String) declares giftCardId but never uses it; remove the unused
parameter from the signature and update its callers (e.g., the call site noted
in the review) to stop passing giftCardId, OR if the dialogs require the
giftCardId for index-aware behavior, pass giftCardId into
GiftCardOrderDetailsDialog.newInstance(...) and
GiftCardDetailsDialog.newInstance(...) and adjust those constructors/factory
methods accordingly (update newInstance signatures and any Dialog classes that
consume it).

564-571: 💤 Low value

Drop the commented-out alternate Text block.

These eight commented-out lines duplicate what EnterAmount now renders. Leaving them in invites drift if the design evolves. Either delete or move to a code review note.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 564 - 571, Remove the commented-out alternate Text block inside
PurchaseGiftCardConfirmDialog (the eight commented lines that render
uiState.purchaseValueText) because EnterAmount now provides that UI; delete
those commented lines so the file only uses EnterAmount and avoid
duplicated/obsolete code, keeping the dialog implementation clean and preventing
drift.
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt (2)

254-280: 💤 Low value

Remove the commented-out total/quantity blocks.

Lines 255-260 and 274-279 each preserve an earlier version of the order-info update logic that's now centralized elsewhere (the setGiftCardOrderInfo call in onContinue for FlexibleSingle, and the direct giftCardOrderInfo.value writes for the multi-card paths). Leaving both copies risks someone re-enabling the dead version during a merge conflict. Drop them and rely on git history if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`
around lines 254 - 280, Remove the dead/commented blocks that compute total/qty
from denominationQuantities and the earlier viewModel updates: in
PurchaseGiftCardFragmentV2 (inside the onContinue lambda handling mode), delete
the commented sections around lines where denominationQuantities.entries.sumOf {
(d, q) -> d * q } and denominationQuantities.values.sum() are preserved so the
code relies only on the existing FlexibleSingle path that parses amountText and
calls viewModel.setGiftCardOrderInfo(fiat, 1) and the existing multi-card/fixed
paths that set giftCardOrderInfo elsewhere; leave setGiftCardOrderInfo,
amountText, and giftCardOrderInfo references intact and rely on git history if
the old logic is needed.

144-196: ⚡ Quick win

Hoist the 2500 cap into a named constant.

2500 appears three times — twice as the gating value (Line 148 and Line 182) and once as the message argument (Line 183). If the cap ever changes, it's easy to miss one of these sites and ship a UI that says "$2,500" while actually accepting a different limit. Centralizing also makes the rule self-documenting.

♻️ Proposed refactor
     companion object {
         private val log = LoggerFactory.getLogger(PurchaseGiftCardFragmentV2::class.java)
+        private const val MAX_MULTIPLE_PURCHASE_USD = 2500.0
     }
@@
-                    totalDouble >= min && totalDouble < fiatBalance.toBigDecimal().toDouble() && !isBlockchainReplaying && totalDouble < 2500 && !exceedsInventory
+                    totalDouble >= min &&
+                        totalDouble < fiatBalance.toBigDecimal().toDouble() &&
+                        !isBlockchainReplaying &&
+                        totalDouble < MAX_MULTIPLE_PURCHASE_USD &&
+                        !exceedsInventory
@@
-                    } else if (totalDouble > 2500.0) {
-                        getString(R.string.purchase_gift_card_max_multiple_error, Fiat.parseFiat(Constants.USD_CURRENCY, 2500.00.toString()).toFormattedString())
+                    } else if (totalDouble > MAX_MULTIPLE_PURCHASE_USD) {
+                        getString(
+                            R.string.purchase_gift_card_max_multiple_error,
+                            Fiat.parseFiat(Constants.USD_CURRENCY, MAX_MULTIPLE_PURCHASE_USD.toString())
+                                .toFormattedString()
+                        )

Same comment applies to the duplicate cap inside PurchaseGiftCardConfirmDialog if/when it lands there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`
around lines 144 - 196, Hoist the hard-coded 2500 into a named constant (e.g.
GIFT_CARD_PURCHASE_CAP_USD) and replace all literal occurrences in this
fragment: the gating check using totalDouble < 2500 (in the flexibleMultiple
branch), the error branch totalDouble > 2500.0, and the Fiat.parseFiat call that
builds the max message (used as the message argument). Define the constant in a
clear scope (a companion object in PurchaseGiftCardFragmentV2 or a shared
Constants file) and reference it where the code currently uses
2500/2500.0/2500.00.toString(); also apply the same constant to
PurchaseGiftCardConfirmDialog when it’s updated to keep behavior and messaging
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/agents/DEVELOPMENT-PATTERNS.md:
- Around line 1457-1460: Remove the stray trailing comma from the property
declaration in the GiftCardDetailsDialog example: locate the class
GiftCardDetailsDialog and the line that declares override val backgroundStyle =
R.style.PrimaryBackground, and change it to a valid Kotlin property declaration
without the comma so it matches the real implementation (also verify override
val forceExpand = true remains unchanged).

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt`:
- Around line 385-401: The option-card branch only sets
thisOrder.first().productId, breaking multi-quantity orders; update the branch
where optionGiftcard != null to assign optionGiftcard.id to every order item
(iterate thisOrder and set orderItem.productId = optionGiftcard.id) so all
entries have the correct productId (symbols: optionGiftcard, thisOrder,
productId, orderItem).
- Around line 519-520: The assigned rate currently calls .toString() on a
nullable exchange rate (exchangeRateMap[data.orderId]?.exchangeRate.toString()
?: "0.0"), which yields the literal "null" and bypasses the ?: fallback; update
the rate expression to only call toString() when the exchangeRate is non-null
(e.g., use a safe-call on exchangeRate like
exchangeRateMap[data.orderId]?.exchangeRate?.toString() ?: "0.0" or use let to
map a non-null exchangeRate to its string), so GiftCardInfo.rate never becomes
the string "null".
- Around line 261-325: Add an explicit is DenominationType.MinMaxMajor -> branch
in the when(type) alongside the existing Fixed and MinMax cases (handle it
similarly to MinMax or with the specific denomination mapping required),
populate giftCardMap and return an UpdatedMerchantDetails with appropriate
denominations, denominationsType, discountPercentage (use SERVICE_FEE like the
other branches), redeemType, enabled check (quantity and disabledMerchants) and
productId; additionally, before the fallback else that currently returns an
empty UpdatedMerchantDetails, add a processLogger.warn (or logger.warn)
indicating the merchant id (it.id) and the missing/ mismatched card shape so
failures are visible; finally run ktlint and adjust formatting to satisfy
repository style.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt`:
- Around line 493-508: The card number ListItem incorrectly uses the PIN copy
handler (onCopyPin), causing the clipboard label and analytics to say "card
pin"; update the call in GiftCardDetailsDialog (the ListItem block that now
calls onCopyPin(giftCard.number!!)) to use a dedicated card-number copy handler
(e.g., onCopyCardNumber or onCopyNumber) and implement/route that handler to
copy the value with the correct label (replace the "card pin" label in the copy
helper with "card number"); apply the same fix to the debug-only order row that
currently reuses onCopyPin so both places use the new/appropriate card-number
copy callback and label.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`:
- Line 401: In PurchaseGiftCardConfirmDialog.kt, avoid the unsafe (!!) when
calling createSendingRequestFromDashUri with
data.first().paymentUrls?.get("DASH.DASH") by null-checking the payment URL
before calling createSendingRequestFromDashUri; e.g. retrieve the url with a
safe call/get, and if it's null show the existing error/user-facing dialog or
return early instead of proceeding, ensuring createSendingRequestFromDashUri is
only invoked with a non-null String.
- Around line 105-115: Rename the companion val currency to USD_CURRENCY and
update NumberFormat initializers (noCentsFormat and currencyFormat) to set
format.currency = USD_CURRENCY (and set minimumFractionDigits using
USD_CURRENCY.defaultFractionDigits where needed) so the formats use USD
regardless of locale; defensively access paymentUrls (replace
data.first().paymentUrls?.get("DASH.DASH")!! with a safe lookup like
data.first().paymentUrls?.get("DASH.DASH") ?: return/handleError to avoid NPE);
remove or use the unused giftCardId parameter in showGiftCardDetailsDialog
(either consume it or delete the parameter) and delete the dead commented-out
Text composable code to clean up the file.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`:
- Around line 323-338: In loadMerchant, avoid the two unsafe !! usages by
checking for nulls and early-returning/handling the error: read the providerName
with viewModel.selectedProvider?.name and if null popBackStack/return; after
getting updated from viewModel.updateMerchantDetails(merchant) safely find the
provider with updated.giftCardProviders.find { it.provider == providerName } and
if that result is null popBackStack or show an error and return; only call
viewModel.setIsFixedDenomination(...) when provider is non-null. Use the
existing viewModel methods (selectedProvider, updateMerchantDetails,
setGiftCardMerchant, setIsFixedDenomination) and updated.giftCardProviders to
locate the checks.

---

Nitpick comments:
In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt`:
- Around line 248-335: The two getMerchant overloads duplicate brand lookup,
disabled-card filtering and card-shape handling; refactor by extracting helpers
such as resolveBrandAndGiftCards(merchantId): Pair<Brand, List<GiftCard>> and
handlers buildFixedMerchantDetails(...), buildOptionMerchantDetails(...),
buildRangeMerchantDetails(... ) that encapsulate the common logic (use symbols
giftCardMap, disabledGiftCards, disabledMerchants, SERVICE_FEE,
UpdatedMerchantDetails), then have getMerchant(merchantId, type:
DenominationType) call those helpers according to DenominationType.Fixed /
MinMax and make the original getMerchant(merchantId) delegate to the overload
(e.g., try Fixed then MinMax) so all filtering, discount math and
enabled/productId logic live in one place.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`:
- Around line 491-507: The function showGiftCardDetailsDialog(txId: Sha256Hash,
giftCardId: String) declares giftCardId but never uses it; remove the unused
parameter from the signature and update its callers (e.g., the call site noted
in the review) to stop passing giftCardId, OR if the dialogs require the
giftCardId for index-aware behavior, pass giftCardId into
GiftCardOrderDetailsDialog.newInstance(...) and
GiftCardDetailsDialog.newInstance(...) and adjust those constructors/factory
methods accordingly (update newInstance signatures and any Dialog classes that
consume it).
- Around line 564-571: Remove the commented-out alternate Text block inside
PurchaseGiftCardConfirmDialog (the eight commented lines that render
uiState.purchaseValueText) because EnterAmount now provides that UI; delete
those commented lines so the file only uses EnterAmount and avoid
duplicated/obsolete code, keeping the dialog implementation clean and preventing
drift.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`:
- Around line 254-280: Remove the dead/commented blocks that compute total/qty
from denominationQuantities and the earlier viewModel updates: in
PurchaseGiftCardFragmentV2 (inside the onContinue lambda handling mode), delete
the commented sections around lines where denominationQuantities.entries.sumOf {
(d, q) -> d * q } and denominationQuantities.values.sum() are preserved so the
code relies only on the existing FlexibleSingle path that parses amountText and
calls viewModel.setGiftCardOrderInfo(fiat, 1) and the existing multi-card/fixed
paths that set giftCardOrderInfo elsewhere; leave setGiftCardOrderInfo,
amountText, and giftCardOrderInfo references intact and rely on git history if
the old logic is needed.
- Around line 144-196: Hoist the hard-coded 2500 into a named constant (e.g.
GIFT_CARD_PURCHASE_CAP_USD) and replace all literal occurrences in this
fragment: the gating check using totalDouble < 2500 (in the flexibleMultiple
branch), the error branch totalDouble > 2500.0, and the Fiat.parseFiat call that
builds the max message (used as the message argument). Define the constant in a
clear scope (a companion object in PurchaseGiftCardFragmentV2 or a shared
Constants file) and reference it where the code currently uses
2500/2500.0/2500.00.toString(); also apply the same constant to
PurchaseGiftCardConfirmDialog when it’s updated to keep behavior and messaging
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d4a7b6d4-3a2a-4240-bf2c-cccb9857206c

📥 Commits

Reviewing files that changed from the base of the PR and between 8f64385 and 02b2213.

📒 Files selected for processing (14)
  • .claude/agents/DEVELOPMENT-PATTERNS.md
  • .claude/agents/figma-to-compose.md
  • common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt
  • common/src/main/res/drawable/ic_chevron_down_small.xml
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ExploreSyncWorker.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/utils/PiggyCardsTestMerchantData.kt
  • features/exploredash/src/main/res/values/strings-explore-dash.xml
✅ Files skipped from review due to trivial changes (3)
  • common/src/main/res/drawable/ic_chevron_down_small.xml
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ExploreSyncWorker.kt
  • features/exploredash/src/main/res/values/strings-explore-dash.xml
🚧 Files skipped from review as they are similar to previous changes (4)
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/utils/PiggyCardsTestMerchantData.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt

Comment thread .claude/agents/DEVELOPMENT-PATTERNS.md
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt (1)

381-408: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle empty purchase responses before calling first().

purchaseGiftCard() now returns a list, but this path assumes at least one element. If a provider ever returns 200 OK with an empty array, the dialog crashes on data.first() instead of showing the existing purchase failure UX.

Suggested fix
             val totalAmount = Coin.valueOf(
                 data.sumOf {
                     if (!it.cryptoAmount.isNullOrEmpty()) {
                         Coin.parseCoin(it.cryptoAmount).value
                     } else {
                         0L
                     }
                 }
             )
+            val firstCard = data.firstOrNull()
+            if (firstCard == null) {
+                hideLoading()
+                log.error("purchaseGiftCard: empty gift card response")
+                if (isAdded) {
+                    AdaptiveDialog.create(
+                        R.drawable.ic_error,
+                        getString(R.string.gift_card_purchase_failed),
+                        getString(R.string.gift_card_error),
+                        getString(R.string.button_close)
+                    ).show(requireActivity())
+                }
+                return@launch
+            }

             if (!totalAmount.isZero && viewModel.needsCrowdNodeWarning(totalAmount)) {
                 if (!isAdded) {
                     hideLoading()
                     return@launch
@@
-            val transactionId = createSendingRequestFromDashUri(data.first().paymentUrls?.get("DASH.DASH")!!)
+            val transactionId = createSendingRequestFromDashUri(firstCard.paymentUrls?.get("DASH.DASH")!!)
             transactionId?.let {
                 enterAmountViewModel.clearSavedState()
                 viewModel.saveGiftCardDummy(transactionId, data)
-                showGiftCardDetailsDialog(transactionId, data.first().id)
+                showGiftCardDetailsDialog(transactionId, firstCard.id)
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 381 - 408, Before calling data.first() check for an empty list and
bail out to the existing purchase-failure flow: if data.isEmpty() then
hideLoading(), invoke the same purchase failure UI/handler used elsewhere in
this class (the existing "purchase failure" UX or method) and return@launch; do
this before the crowd-node warning and before calling
createSendingRequestFromDashUri so you never call data.first() or dereference
paymentUrls on an empty list (also guard the payment URL lookup or handle a
missing "DASH.DASH" entry before passing it to createSendingRequestFromDashUri).
🧹 Nitpick comments (2)
features/exploredash/src/main/res/drawable/ic_stepper_minus.xml (1)

8-8: 💤 Low value

Consider using a theme color attribute instead of a hardcoded hex.

#191C1F is hardcoded and won't adapt to dark mode or dynamic theming. Referencing a color resource (e.g., @color/dash_gray_1 or ?attr/colorOnSurface) keeps the icon consistent with the app's theme system, especially important for a stepper control that appears in interactive UI.

🎨 Suggested change
-        android:strokeColor="#191C1F"
+        android:strokeColor="@color/dash_gray_1"

(Apply the same fix to ic_stepper_plus.xml.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/exploredash/src/main/res/drawable/ic_stepper_minus.xml` at line 8,
Replace the hardcoded stroke color android:strokeColor="#191C1F" in
ic_stepper_minus.xml with a theme-aware color reference (e.g., use a color
resource like `@color/dash_gray_1` or an attribute like ?attr/colorOnSurface) so
the icon adapts to dark mode and dynamic themes; apply the same change to
ic_stepper_plus.xml and ensure both XML drawables reference the chosen color
attribute/resource instead of the hex literal.
common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt (1)

53-63: ⚡ Quick win

Guard processAmountKeyInput() against unsupported key values.

Unknown key values currently fall into else and get appended to the amount string. That can introduce invalid amount states if this helper is reused outside this keypad. Prefer explicit key whitelisting and no-op on unsupported keys.

Suggested diff
 fun processAmountKeyInput(current: String, key: String, maxDecimalPlaces: Int = 2): String {
+    val isDigitKey = key.length == 1 && key[0].isDigit()
     return when (key) {
         "back" -> if (current.length > 1) current.dropLast(1) else "0"
         "back_long" -> "0"
         "." -> if (current.contains('.')) current else "$current."
-        else -> {
+        else -> if (isDigitKey) {
             val result = if (current == "0") key else current + key
             val dotIndex = result.indexOf('.')
             if (dotIndex != -1 && result.length - dotIndex - 1 > maxDecimalPlaces) current else result
-        }
+        } else current
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt`
around lines 53 - 63, The function processAmountKeyInput currently treats any
unknown key as a digit and appends it, which can create invalid amounts; update
processAmountKeyInput to explicitly whitelist allowed keys (digits 0-9, ".",
"back", "back_long") and treat any other key as a no-op returning the current
value; implement the digit-path only when key.matches(0-9) and keep existing
logic for "." and back/back_long and decimal-place checks so unsupported inputs
no longer mutate the amount.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt`:
- Around line 281-285: Replace the hardcoded TalkBack strings in the
contentDescription for the eye-toggle with localized string resources: add
string keys (e.g., hide_balance and show_balance) to strings.xml and use
stringResource(...) in the TopIntroSend composable where contentDescription is
set (the conditional that checks isVisible) so the accessibility labels are
translated instead of being hardcoded.

In
`@common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt`:
- Around line 119-123: The Icon shown for the backspace key in
EnterAmountCompose (inside the isBack branch rendering
painterResource(R.drawable.ic_delete_backward)) currently uses
contentDescription = null; update this to provide an accessible label by using a
localized string via contentDescription =
stringResource(R.string.accessibility_delete) (and add
R.string.accessibility_delete to your strings.xml if it doesn't exist), so
screen readers can announce the delete/backspace control.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt`:
- Around line 455-464: The PiggyCards test-merchant branch calls
(piggyCardsRepository as PiggyCardsRepository).getMerchant(...) but discards the
return value; capture the returned merchant/provider response and add it into
merchantResponseList and providerResponseList in the same way other branches do
so updateMerchantDetailsForAllProviders() sees the PiggyCards data. Locate the
branch guarded by SUPPORT_PIGGY_CARDS_TEST_MERCHANT and
PiggyCardsTestMerchants.ALL.find { it.merchantId == merchant.merchantId } and
after calling getMerchant(provider.sourceId, DenominationType.fromString(...))
append the non-null result into merchantResponseList and providerResponseList
(matching how other code paths populate those collections) so the subsequent
call to updateMerchantDetailsForAllProviders() includes the PiggyCards
test-merchant data.
- Around line 190-197: The public MutableStateFlow properties
isFixedDenominationMultiple and giftCardOrderInfo should be made private
MutableStateFlow-backed properties with public immutable StateFlow accessors
(use asStateFlow()), mirroring the pattern used for
_isFixedDenomination/isFixedDenomination; rename the mutables (e.g.,
_isFixedDenominationMultiple, _giftCardOrderInfo) and expose val
isFixedDenominationMultiple: StateFlow<Boolean?> and val giftCardOrderInfo:
StateFlow<Map<Double,Int>>; remove direct external mutation from
PurchaseGiftCardFragmentV2 by adding ViewModel update methods (e.g.,
setIsFixedDenominationMultiple(value: Boolean?) and updateGiftCardOrderInfo(...)
or specific add/remove methods) that encapsulate and validate state changes
before updating the private MutableStateFlow(s).

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`:
- Around line 348-353: After calling viewModel.updateMerchantDetails(merchant)
the code checks the stale viewModel.giftCardMerchant.value; change the guard to
inspect the returned updated merchant instead (e.g., check updated?.active !=
true) and if it's inactive, popBackStack and return; only call
viewModel.setGiftCardMerchant(updated) after confirming updated is active.
Reference: updateMerchantDetails, updated, viewModel.giftCardMerchant,
viewModel.setGiftCardMerchant.
- Around line 160-167: The enablement logic for
GiftCardPurchaseMode.FlexibleMultiple and Fixed can become true with an empty
cart; add a check that at least one card is selected before allowing Continue.
In the branch handling GiftCardPurchaseMode.FlexibleMultiple and Fixed (look for
denominationQuantities, exceedsInventory, minFiat, totalDouble, fiatBalance,
isBlockchainReplaying and merchant), require denominationQuantities.any {
it.value > 0 } (or denominationQuantities.values.sum() > 0) as an additional AND
condition so the whole expression also ensures there is at least one selected
card.
- Around line 371-389: The generated denominations list in
PurchaseGiftCardFragmentV2.kt can contain duplicates or values outside the
merchant bounds; update the code that builds the denominations (the local
denominations list used to construct GiftCardPurchaseMode.FlexibleMultiple) to:
clamp each candidate value to the range [minimum, maximum], deduplicate them
(preserve order or sort) before creating the FlexibleMultiple, and apply the
same clamping/dedupe logic inside the PiggyCards test-merchant branch as well;
consider deduping doubles with a stable approach (e.g., round to cents or use a
LinkedHashSet of rounded values) so rows aren’t repeated and no denomination
exceeds merchant.maxCardPurchase.

---

Outside diff comments:
In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`:
- Around line 381-408: Before calling data.first() check for an empty list and
bail out to the existing purchase-failure flow: if data.isEmpty() then
hideLoading(), invoke the same purchase failure UI/handler used elsewhere in
this class (the existing "purchase failure" UX or method) and return@launch; do
this before the crowd-node warning and before calling
createSendingRequestFromDashUri so you never call data.first() or dereference
paymentUrls on an empty list (also guard the payment URL lookup or handle a
missing "DASH.DASH" entry before passing it to createSendingRequestFromDashUri).

---

Nitpick comments:
In
`@common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt`:
- Around line 53-63: The function processAmountKeyInput currently treats any
unknown key as a digit and appends it, which can create invalid amounts; update
processAmountKeyInput to explicitly whitelist allowed keys (digits 0-9, ".",
"back", "back_long") and treat any other key as a no-op returning the current
value; implement the digit-path only when key.matches(0-9) and keep existing
logic for "." and back/back_long and decimal-place checks so unsupported inputs
no longer mutate the amount.

In `@features/exploredash/src/main/res/drawable/ic_stepper_minus.xml`:
- Line 8: Replace the hardcoded stroke color android:strokeColor="#191C1F" in
ic_stepper_minus.xml with a theme-aware color reference (e.g., use a color
resource like `@color/dash_gray_1` or an attribute like ?attr/colorOnSurface) so
the icon adapts to dark mode and dynamic themes; apply the same change to
ic_stepper_plus.xml and ensure both XML drawables reference the chosen color
attribute/resource instead of the hex literal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4fadd4b4-a1b5-40bf-be83-2917fd02bb2a

📥 Commits

Reviewing files that changed from the base of the PR and between 02b2213 and 7bf5610.

📒 Files selected for processing (9)
  • common/src/main/java/org/dash/wallet/common/ui/components/TopIntroSend.kt
  • common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountCompose.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt
  • features/exploredash/src/main/res/drawable/ic_stepper_minus.xml
  • features/exploredash/src/main/res/drawable/ic_stepper_plus.xml
  • features/exploredash/src/main/res/values/strings-explore-dash.xml

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt (2)

381-405: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the purchase response before calling first().

purchaseGiftCard() is an external call, but the success path assumes data is non-empty. If the backend ever returns [], both data.first() calls crash instead of showing the existing failure UI.

💡 Minimal fix
             val data = try {
                 viewModel.purchaseGiftCard()
             } catch (ex: CTXSpendException) {
                 hideLoading()
                 when {
@@
                 }
                 return@launch
             }
+
+            if (data.isEmpty()) {
+                hideLoading()
+                log.error("purchaseGiftCard: empty response")
+                if (isAdded) {
+                    AdaptiveDialog.create(
+                        R.drawable.ic_error,
+                        getString(R.string.gift_card_purchase_failed),
+                        getString(R.string.gift_card_error),
+                        getString(R.string.button_close)
+                    ).show(requireActivity())
+                }
+                return@launch
+            }
 
             val totalAmount = Coin.valueOf(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 381 - 405, The code assumes the purchase response list "data" is
non-empty and calls data.first() which can crash; before using data.first() (and
before computing totalAmount), guard the response by checking data.isNotEmpty()
or using data.firstOrNull() and handle the empty case by calling hideLoading(),
showing the existing failure UI/path and returning from the coroutine; update
the block around the totalAmount computation, the needsCrowdNodeWarning check,
and the createSendingRequestFromDashUri(...) call so they only run when data has
at least one element (or handle null transactionId as now).

198-210: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a re-entrancy guard before starting the purchase flow.

This handler can be entered twice before showLoading() runs inside the coroutine, which makes duplicate purchase submissions possible on a fast double-tap.

💡 Minimal fix
 private fun onConfirmButtonClicked() {
+    if (_uiState.value.isLoading) return
+    showLoading()
     viewLifecycleOwner.lifecycleScope.launch {
         // Double-check merchant is still available before proceeding
         if (viewModel.giftCardMerchant.value == null) {
+            hideLoading()
             log.warn("PurchaseGiftCardConfirmDialog: Merchant became null during confirmation, dismissing")
             dismiss()
             return@launch
         }
-        showLoading()
         if (!isAdded || authManager.authenticate(requireActivity()) == null) {
             hideLoading()
             return@launch
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`
around lines 198 - 210, The onConfirmButtonClicked handler can be entered twice
causing duplicate purchases; add a re-entrancy guard (e.g., a boolean/atomic
flag like isPurchasing) checked and set at the start of onConfirmButtonClicked
(or immediately inside viewLifecycleOwner.lifecycleScope.launch before
showLoading()) and cleared on all exit paths (after hideLoading/dismiss or on
failure), so subsequent invocations return early; reference the
onConfirmButtonClicked method, showLoading()/hideLoading(), and
authManager.authenticate(...) when adding and clearing the guard to ensure
duplicate submissions are prevented.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt`:
- Around line 38-40: The file GiftCardDetailsDialog.kt has ktlint failures:
remove the unused Compose import (e.g., delete rememberScrollState import if
unused), remove the trailing comma in the previewState(...) call, break up or
shorten the overlong preview invocations so they fit line-length limits, ensure
the file ends with a newline, and reformat the file with ktlint/ktfmt (or run
the project's ktlintMainSourceSetCheck) to apply consistent formatting; adjust
references in previews and composables as needed (search for previewState(...)
and the preview functions) to satisfy the linter.
- Around line 219-223: The timeout log message is hardcoded to "after 10 tries"
and is out of sync with the actual threshold; update the onErrorLogged call in
the LaunchedEffect block (where uiState.error, uiState.queries,
waitLimitForError are checked) so the message uses the real threshold value
(e.g., interpolate waitLimitForError or the constant WAIT_LIMIT_FOR_ERROR)
instead of the literal "10" to keep logs accurate for support.

---

Outside diff comments:
In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt`:
- Around line 381-405: The code assumes the purchase response list "data" is
non-empty and calls data.first() which can crash; before using data.first() (and
before computing totalAmount), guard the response by checking data.isNotEmpty()
or using data.firstOrNull() and handle the empty case by calling hideLoading(),
showing the existing failure UI/path and returning from the coroutine; update
the block around the totalAmount computation, the needsCrowdNodeWarning check,
and the createSendingRequestFromDashUri(...) call so they only run when data has
at least one element (or handle null transactionId as now).
- Around line 198-210: The onConfirmButtonClicked handler can be entered twice
causing duplicate purchases; add a re-entrancy guard (e.g., a boolean/atomic
flag like isPurchasing) checked and set at the start of onConfirmButtonClicked
(or immediately inside viewLifecycleOwner.lifecycleScope.launch before
showLoading()) and cleared on all exit paths (after hideLoading/dismiss or on
failure), so subsequent invocations return early; reference the
onConfirmButtonClicked method, showLoading()/hideLoading(), and
authManager.authenticate(...) when adding and clearing the guard to ensure
duplicate submissions are prevented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6da1651-4976-4a03-a066-d88ab2052078

📥 Commits

Reviewing files that changed from the base of the PR and between 7bf5610 and 546e89a.

📒 Files selected for processing (3)
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/agents/DEVELOPMENT-PATTERNS.md:
- Around line 1477-1480: The comment for ctxSpendViewModel is incorrect: it says
"Activity-scoped ViewModel..." but the property uses by
viewModels<DashSpendViewModel>() which creates a dialog/fragment-scoped
instance; either change the delegate to by
activityViewModels<DashSpendViewModel>() to make it truly activity-scoped, or
update the comment to explicitly state that ctxSpendViewModel uses by viewModels
and is dialog/fragment-scoped (and note intended usage/fire-and-forget behavior)
so readers aren’t misled.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt`:
- Around line 793-795: Replace the hard-coded real-looking card PAN
"6006491727005748" used for the gift card number and barcode in
GiftCardDetailsDialog with an obviously fake test value (e.g., repeating digits
or all zeros) so static analysis won't flag it; update both places where the
variable/field named number and barcode are set to that literal, keeping the
same format as other examples in the file (ensure tests/previews use the new
clearly fake value).
- Around line 120-121: The DashSpendViewModel is being created with dialog scope
using viewModels() in GiftCardDetailsDialog; replace that with the nav-graph
scoped exploreViewModels<DashSpendViewModel>() so the dialog shares the same
DashSpendViewModel instance used by PurchaseGiftCardFragment,
DashSpendTermsDialog, PurchaseGiftCardConfirmDialog, SearchFragment, and so that
calls on ctxSpendViewModel.logError() and ctxSpendViewModel.createEmailIntent()
operate on the shared state. Locate the property declaration private val
ctxSpendViewModel by viewModels<DashSpendViewModel>() in GiftCardDetailsDialog
and change it to use exploreViewModels<DashSpendViewModel>().

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt`:
- Around line 437-450: The dummy-card creation copies every field from
cardToCopy which preserves sensitive fields (number, pin, barcodeValue,
merchantUrl/redeemUrl); change the added creation so each new card only inherits
the index and explicitly clears sensitive fields (e.g., set number=null,
pin=null, barcodeValue=null, merchantUrl=null/redeemUrl=null as appropriate)
before calling giftCardDao.insertGiftCard and updating _uiState; also ensure the
updateGiftCard path that writes merchantUrl/redeemUrl explicitly clears
number/pin/barcodeValue for that index so DB entries cannot retain stale data.
- Around line 543-558: The code is indexing uiState.value.giftCards by the
loop/parameter "index" (a position) which can mismatch the GiftCard.index field
and cause OOB or corruption; update the code in functions like updateGiftCard
and the metadataProvider.updateGiftCardMetadata call to locate the card by its
GiftCard.index (e.g., find or first { it.index == index }) rather than using
giftCards[index], and when creating copies (the giftCard.copy(...) calls used to
update number/pin) do not reassign the index field—only copy number and pin (and
other mutable fields) so the stored .index is preserved; also handle the case
where the find returns null (guard or early return) to avoid crashes.

In
`@features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt`:
- Around line 259-263: The try/catch around Fiat.parseFiat in
PurchaseGiftCardFragmentV2 (the fiatAmount assignment and the similar block
later) currently swallows the exception; update both blocks to catch the
exception but log the caught exception (at debug) before returning null so parse
failures are visible in logs during onContinue handling. Locate the
Fiat.parseFiat(...) calls in PurchaseGiftCardFragmentV2 (used to produce
fiatAmount) and add a debug log statement that includes the exception
message/stacktrace via your app's logger (e.g., Timber/Log/fragment logger)
inside the catch, then keep returning null to preserve behavior.
- Line 158: The boolean expression in PurchaseGiftCardFragmentV2 (the line
containing "amount > BigDecimal.ZERO && amount >= min && amount <= max && amount
< balanceMax && !isBlockchainReplaying") exceeds ktlint's 120-char limit; split
the chained conditions across multiple lines (one condition per line or grouped
logically) and align them with indentation so the expression remains under 120
chars, ensuring you keep the same logical order and use the same variable names
(amount, min, max, balanceMax, isBlockchainReplaying) and any surrounding
parentheses in the method where this check appears.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b8f3afd-c886-4bf3-96ea-5e2cd970496b

📥 Commits

Reviewing files that changed from the base of the PR and between 546e89a and 78aec35.

📒 Files selected for processing (18)
  • .claude/agents/DEVELOPMENT-PATTERNS.md
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ExploreSyncWorker.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/DashSpendRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendUserAuthFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/explore/SearchFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/utils/PiggyCardsTestMerchantData.kt
✅ Files skipped from review due to trivial changes (2)
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/utils/PiggyCardsTestMerchantData.kt
🚧 Files skipped from review as they are similar to previous changes (12)
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/explore/SearchFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendUserAuthFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/DashSpendRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ExploreSyncWorker.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt
  • features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardScreenV2.kt

Comment thread .claude/agents/DEVELOPMENT-PATTERNS.md
1. ignoring PC if logged into CTX
2. discount and fees for negative values
3. purchase confirmation dialog clipping buttons

exploreConfig.saveExploreDatabasePrefs(databasePrefs)

addPiggyCardsTestMerchantIfNeeded()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like real users will see test merchants in master?

val metadataItem = TransactionMetadataCacheItem(
metadata,
giftCard
giftCard.first()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.firstOrNull() as well here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants