Skip to content

feat(explore-dash): refactor confirmation and details sheet flow#769

Open
romchornyi wants to merge 15 commits into
fix/support-iOS-26from
mo-896-add-quantity-confirmation-card-details
Open

feat(explore-dash): refactor confirmation and details sheet flow#769
romchornyi wants to merge 15 commits into
fix/support-iOS-26from
mo-896-add-quantity-confirmation-card-details

Conversation

@romchornyi
Copy link
Copy Markdown
Contributor

@romchornyi romchornyi commented May 5, 2026

Issue being fixed or feature implemented

MO-896 — refactor DashSpend confirmation/card-details flow and improve sheet behavior for gift card details presentation.

What was done?

  • Refactored Gift Card Details into a dedicated module structure:
    • added GiftCardDetails/ folder with:
      • GiftCardDetailsView
      • GiftCardDetailsViewModel
      • componentized sections (InfoCard, MerchantHeader, HowToUseSection, PoweredBySection, PurchaseSelectionSheet)
    • removed legacy flat GiftCardDetailsView(.swift/.ViewModel.swift) files in old location
  • Updated DashSpend confirmation flow UI:
    • integrated updated confirmation dialog usage
    • aligned sheet presentation behavior and layout to the new component structure
  • Integrated new Gift Card Details flow into Home navigation/presentation path:
    • updated HomeView/HomeViewController routing and sheet wiring
  • Added supporting design tokens/colors used by the new card-details presentation:
    • Black1000Alpha5
    • Black1000Alpha8
  • Updated localized strings required by the new confirmation/details UX.

How Has This Been Tested?

  • Manual testing on iOS Simulator:
    • opened merchant details and launched Gift Card Details sheet
    • verified card-details screen sections render correctly (header/info/how-to-use/powered-by)
    • verified purchase selection sheet opens/closes and transitions correctly
    • verified confirmation dialog appears and actions (confirm/cancel) behave correctly
    • verified navigation back/dismiss paths from card-details flow
  • Regression checks:
    • existing home flow still opens relevant DashSpend screens
    • no crashes on switching between details and confirmation states

Breaking Changes

  • None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Enhanced gift‑card purchase UI: single/multiple modes, denomination rows, stepper, numeric keyboard, segmented chooser, multi‑item ordering, confirmation sheet, and formatted amount display
    • New gift‑card details screens with barcode/claim‑link support and “How to use”/powered‑by sections
    • Added image and color assets
  • Improvements

    • Inventory tracking, weighted discounts, validation and error handling, network/auth flows, and visual/theme tweaks
    • New localization strings
  • Bug Fixes

    • Safer explore database sync when local DB is missing

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e1593ee-bb3e-4156-991b-66a2e3349527

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds multi-quantity PiggyCards ordering, a gift-card details polling view-model + barcode support, many new/refactored SwiftUI components, asset/color/localization updates, DB test-data insertion, and sheet/detent presentation changes.

Changes

Gift Card Purchase & Details Flow

Layer / File(s) Summary
Assets, colors, localization
DashWallet/Resources/AppAssets.xcassets/*, Shared/Resources/SharedAssets.xcassets/Colors/*, DashWallet/en.lproj/Localizable.strings
New image asset Contents.json files, updated shared color Contents.json files, and added DashSpend localization strings.
Database & Sync
.../Infrastructure/Database Connection/ExploreDatabaseConnection.swift, .../Services/ExploreDatabaseSyncManager.swift
Bulk transactional insertion for multiple PiggyCards test merchants with FTS trigger teardown/recreate; sync now checks for local DB presence and forces cloud download if missing.
Models & repository
.../Model/Entites/CTXSpendModels.swift, .../Services/DashSpend/PiggyCards/PiggyCardsRepository.swift, .../Model/DashSpend/PiggyCardsConstants.swift
MerchantResponse decoding made robust for multiple API variants and formats; PiggyCardsRepository.orderGiftCards supports multi-line items and computes weighted basket discounts; added PiggyCardsConstants.maxOrderAmount.
View Models & Purchase Flow
.../Views/DashSpend/DashSpendPayViewModel.swift, .../Models/Transactions/SendCoinsService.swift
DashSpendPayViewModel extended for quantities, inventory, serialized PiggyCards order notes, validation and updated purchase API signatures; SendCoinsService adds MainActor-wrapped payment authentication helpers.
GiftCard Details VM & Components
.../GiftCardDetails/GiftCardDetailsViewModel.swift, .../GiftCardDetails/*
New GiftCardDetailsViewModel with polling for CTX/PiggyCards and barcode generation; modular components for merchant header, info card, powered-by, how-to-use, and purchase selection sheet.
Purchase UI Components & Screens
.../DashSpend/Components/*, .../DashSpend/DashSpendPayScreen.swift, .../Home/*
New/refactored SwiftUI components for denomination selection, steppers, segmented control, numeric keyboard, fixed/flexible/multiple panels, confirmation dialog; DashSpendPayScreen refactored to use quantities and confirmation snapshot; HomeView/HomeViewController updated for multi-step sheet routing and detent sizing.
UI Framework & Styling
.../SwiftUI Components/BottomSheet.swift, .../Button.swift, .../Color+DWStyle.swift
BottomSheet split into subviews with theme-aware icons; DashButton dark-mode disabled colors; new color constants and previews.

Sequence Diagram(s)

sequenceDiagram
  participant UI as User Interface
  participant VM as DashSpendPayViewModel
  participant Repo as PiggyCardsRepository
  participant API as PiggyCards API
  participant DB as Local Database

  UI->>VM: present selectedQuantities & confirm
  VM->>Repo: orderGiftCards(lineItems)
  Repo->>API: POST orders (multiple)
  API-->>Repo: order response
  Repo->>VM: processOrderResponse(summaryDiscount)
  VM->>DB: save GiftCard with note (order metadata)
  DB-->>VM: persisted
  VM-->>UI: purchase success / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • HashEngineering

Poem

🐰 Hop, hop — cards in a row,

Quantities counted, steppers go,
Polls and barcodes hum all night,
Sheets and detents tucked in tight,
A rabbit cheers: interfaces bright!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mo-896-add-quantity-confirmation-card-details

@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@romchornyi romchornyi changed the title feat(giftcard): refactor confirmation and details sheet flow feat(explore-dash): refactor confirmation and details sheet flow May 5, 2026
@romchornyi romchornyi force-pushed the mo-896-add-quantity-confirmation-card-details branch from e95d40c to 81e7afb Compare May 5, 2026 11:32
@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 13

Note

Due to the large number of review comments, Critical, Major 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 (1)
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift (1)

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

More empty conditional bodies (likely stripped debug logging).

Lines 304-306 and 318-321 contain if let ... { } and if case .statusCode { if let ... { } } with empty bodies. These produce zero behavior but still encode/serialize JSON and decode response data on the failure path, paying that cost for nothing. Either delete these blocks or wrap a real DSLogger.log("…") call inside #if DEBUG.

🧹 Proposed cleanup
-        // Log the JSON request for debugging
-        if let jsonData = try? JSONEncoder().encode(orderRequest),
-           let jsonString = String(data: jsonData, encoding: .utf8) {
-        }
-
         do {
             let orderResponse: PiggyCardsOrderResponse = try await PiggyCardsAPI.shared.request(.createOrder(orderRequest))
             …
         } catch let error as HTTPClientError {
-            // Log the raw error response for debugging
-            if case .statusCode(let response) = error {
-                if let errorString = String(data: response.data, encoding: .utf8) {
-                }
-            }
             throw try parseError(from: error, context: "create order")

As per coding guidelines: Wrap debug print statements in #if DEBUG blocks or use conditional compilation to prevent performance impact from excessive logging.

🤖 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 `@DashWallet/Sources/Models/Explore`
Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift around lines 303 -
321, Remove the no-op conditional blocks that decode/encode JSON with empty
bodies or convert error response data to strings; either delete the if-let
blocks around encoding orderRequest (the empty block after
JSONEncoder().encode(orderRequest) / String(...)) and the nested if-let that
converts response.data inside the HTTPClientError .statusCode case, or replace
them with real debug logging wrapped in conditional compilation (e.g., inside
`#if` DEBUG use DSLogger.log(...) referencing orderRequest and the response.data
string). Ensure you keep the core flow that awaits
PiggyCardsAPI.shared.request(.createOrder(orderRequest)) and the call to
processOrderResponse, and reference the HTTPClientError handling branch (case
.statusCode(let response)) when adding any debug log so you only incur the cost
in DEBUG builds.
🟡 Minor comments (11)
DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift-234-295 (1)

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

Escape or bind these SQL values before interpolation.

These statements interpolate merchant names, URLs, ids, and city/state values directly into SQL. One apostrophe in future fixture data will break the whole transaction and leave none of the test merchants inserted. Please switch to bound parameters or at least escape SQL string literals in one place before composing the queries.

Suggested hardening
 private func addPiggyCardsTestMerchants(db: Connection) {
     do {
-        let allIds = piggyCardsTestMerchants.map { "'\($0.merchantId)'" }.joined(separator: ", ")
+        func sqlLiteral(_ value: String) -> String {
+            "'\(value.replacingOccurrences(of: "'", with: "''"))'"
+        }
+
+        let allIds = piggyCardsTestMerchants.map { sqlLiteral($0.merchantId) }.joined(separator: ", ")

         try db.transaction {
             // Drop FTS triggers to allow merchant table modifications
             try db.run("DROP TRIGGER IF EXISTS room_fts_content_sync_merchant_fts_AFTER_INSERT")
@@
                 // Insert all test merchants
                 for m in piggyCardsTestMerchants {
-                    let territory = "'\(m.territory ?? "")'"
-                    let city = "'\(m.city ?? "")'"
+                    let territory = sqlLiteral(m.territory ?? "")
+                    let city = sqlLiteral(m.city ?? "")

                     try db.run("""
                         INSERT INTO merchant (
@@
                         ) VALUES (
-                            '\(m.merchantId)', '\(m.name)', 'PiggyCards', '\(m.sourceId)',
-                            '\(m.logo)', 1, 'gift card', \(m.merchantSavings),
-                            '\(m.merchantDenomType)', 'online', 'online',
-                            \(territory), \(city), '\(m.website)',
+                            \(sqlLiteral(m.merchantId)), \(sqlLiteral(m.name)), 'PiggyCards', \(sqlLiteral(m.sourceId)),
+                            \(sqlLiteral(m.logo)), 1, 'gift card', \(m.merchantSavings),
+                            \(sqlLiteral(m.merchantDenomType)), 'online', 'online',
+                            \(territory), \(city), \(sqlLiteral(m.website)),
                             datetime('now'), datetime('now')
                         )
                     """)
@@
                         ) VALUES (
-                            '\(m.merchantId)', 'PiggyCards', '\(m.sourceId)',
-                            \(m.providerSavings), '\(m.providerDenomType)', 1, 'online'
+                            \(sqlLiteral(m.merchantId)), 'PiggyCards', \(sqlLiteral(m.sourceId)),
+                            \(m.providerSavings), \(sqlLiteral(m.providerDenomType)), 1, 'online'
                         )
                     """)

-                    try db.run("INSERT INTO merchant_fts(docid, name) VALUES (\(rowId), '\(m.name)')")
+                    try db.run("INSERT INTO merchant_fts(docid, name) VALUES (\(rowId), \(sqlLiteral(m.name)))")
                 }
🤖 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 `@DashWallet/Sources/Models/Explore` Dash/Infrastructure/Database
Connection/ExploreDatabaseConnection.swift around lines 234 - 295, The SQL in
addPiggyCardsTestMerchants interpolates raw strings (merchantId, name, city,
territory, website, etc.) which can break or allow injection; change to use
parameterized/bound queries instead of string interpolation: build DELETE/INSERT
statements with placeholders and bind arrays of values (or execute per-merchant
deletes/inserts) — e.g., replace the IN (...) string interpolation for allIds
with a parameterized IN clause or loop and bind each merchantId, and convert the
INSERTs into prepared statements that bind m.merchantId, m.name, m.sourceId,
m.logo, m.merchantSavings, m.merchantDenomType, m.website, m.city, m.territory,
m.providerSavings, m.providerDenomType, etc.; also bind the docid for
merchant_fts instead of interpolating rowId. Ensure you use db.run/sqlite
prepared statement APIs that accept parameters and remove any single-quote
string concatenation.
DashWallet/en.lproj/Localizable.strings-2968-2970 (1)

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

Handle singular/plural for card count text

Line 2969 will produce incorrect text for 1 ("1 cards"). Please use a pluralized string (e.g., .stringsdict) or at least separate singular/plural keys.

🤖 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 `@DashWallet/en.lproj/Localizable.strings` around lines 2968 - 2970, The
existing localization key "Merchant has only %d cards available" will render "1
cards" for singular; update localization to support pluralization by replacing
this single string with a localized plural entry (use an .stringsdict) or at
minimum add two keys (e.g., "Merchant has only %d card available" and "Merchant
has only %d cards available") and switch selection in code where this message is
used to choose singular vs plural based on the count; ensure the original key
referenced in code (the "Merchant has only %d cards available" key) is updated
to the new pluralized key names or that code is changed to fetch the
.stringsdict plural form.
DashWallet/Sources/UI/Main/MainTabbarController.swift-81-81 (1)

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

paymentIsOpened flag is set but never read — double-presentation is not guarded

The flag is updated faithfully in showPaymentsController and closePayments, but there is no early-return in showPaymentsController when paymentIsOpened == true. Rapid double-taps on the payment tab (which bypasses shouldSelect twice before the modal is fully presented) can trigger two concurrent present calls on selectedViewController?.topController(). UIKit will emit a runtime warning and silently drop the second presentation, but the flag was clearly intended to prevent this.

🔒 Proposed guard
 func showPaymentsController(withActivePage pageIndex: PaymentsViewControllerState) {
+    guard !paymentIsOpened else { return }
     paymentIsOpened = true

Also applies to: 390-413

🤖 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 `@DashWallet/Sources/UI/Main/MainTabbarController.swift` at line 81, Add an
early-return guard in showPaymentsController to check the paymentIsOpened Bool
before attempting to present to prevent double presentation; specifically, in
the showPaymentsController function, if paymentIsOpened == true return
immediately, otherwise set paymentIsOpened = true right before calling
selectedViewController?.topController().present(...), and ensure closePayments
sets paymentIsOpened = false when the payment modal is dismissed; reference the
paymentIsOpened flag, showPaymentsController and closePayments and the
presentation target selectedViewController?.topController() when applying the
change.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendDenominationRow.swift-23-23 (1)

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

Remove redundant = nil from optional property (SwiftLint implicit_optional_initialization)

SwiftLint warns on explicit = nil initialisation of an optional property.

🔧 Fix
-    var inventoryLimit: Int? = nil
+    var inventoryLimit: Int?
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendDenominationRow.swift at line 23, The
declaration var inventoryLimit: Int? = nil is redundantly initializing an
optional to nil; remove the explicit "= nil" so the property is declared as var
inventoryLimit: Int? (update the DashSpendDenominationRow optional property
accordingly to satisfy SwiftLint's implicit_optional_initialization rule).
DashWallet/Sources/UI/SwiftUI Components/DashStepper.swift-21-36 (1)

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

SwiftLint empty_count errors will break CI — rename count to quantity

SwiftLint's empty_count rule fires on count > 0 at lines 33 and 36 because the binding is named count. These are reported as [Error], which typically gates CI. The variable represents a quantity, not a collection count, so quantity is the semantically correct name and immediately eliminates the false positive without a suppression comment.

♻️ Rename `count` → `quantity` throughout the file
 struct DashStepper: View {
-    `@Binding` var count: Int
+    `@Binding` var quantity: Int
     var maxCount: Int?
 
     private var plusEnabled: Bool {
         guard let max = maxCount else { return true }
-        return count < max
+        return quantity < max
     }
 
     var body: some View {
         HStack(spacing: 6) {
             stepperButton(
                 systemImage: "minus",
-                enabled: count > 0,
+                enabled: quantity > 0,
                 accessibilityLabel: NSLocalizedString("Decrease quantity", comment: "DashSpend")
             ) {
-                if count > 0 { count -= 1 }
+                if quantity > 0 { quantity -= 1 }
             }
 
             Text("\(count)")
+            Text("\(quantity)")
 
             stepperButton(
                 systemImage: "plus",
                 enabled: plusEnabled,
                 accessibilityLabel: NSLocalizedString("Increase quantity", comment: "DashSpend")
             ) {
-                count += 1
+                quantity += 1
             }
         }
     }
     // ...
-    .accessibilityValue(Text("\(count)"))
+    .accessibilityValue(Text("\(quantity)"))

The call site in DashSpendDenominationRow would change to:

-DashStepper(count: $count, maxCount: inventoryLimit)
+DashStepper(quantity: $count, maxCount: inventoryLimit)
🤖 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 `@DashWallet/Sources/UI/SwiftUI` Components/DashStepper.swift around lines 21 -
36, Rename the Binding and all uses from count to quantity to avoid the
SwiftLint empty_count false positive: change the property declaration `@Binding`
var count: Int to `@Binding` var quantity: Int, update maxCount comparison logic
in the computed property plusEnabled (return quantity < max), update all checks
and mutations inside body (enabled: quantity > 0, if quantity > 0 { quantity -=
1 }, and any increments where used), and update any callers such as
DashSpendDenominationRow to pass and read the renamed binding (quantity)
accordingly.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift-86-92 (1)

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

Mask string accessibility / consistency nit.

Text("***********") is a fixed-length 11-asterisk string. It will be read by VoiceOver as "asterisk" repeated, and visually it doesn't track the actual amount width. Two small improvements:

  • give the masked text an .accessibilityLabel(NSLocalizedString("Balance hidden", comment: "")) so VoiceOver announces the state, and
  • consider using a single redacted view (DashAmount(...).redacted(reason: .placeholder) together with the fiat) so the masked layout matches the unmasked layout.
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendPayIntro.swift around lines 86 - 92,
Replace the fixed literal mask Text("***********") with an accessible,
layout-consistent placeholder: when balanceHidden is true, render the same
DashAmount(amount:…, font: .subheadline, showDirection: false) and the fiat view
but apply .redacted(reason: .placeholder) to those views so layout matches the
unmasked state, and add .accessibilityLabel(NSLocalizedString("Balance hidden",
comment: "")) to the masked container (or the DashAmount when redacted) so
VoiceOver reads a meaningful message; update the conditional branch that
currently uses Text("***********") to use these changes (refer to balanceHidden,
DashAmount(...) and formattedFiatText(...)).
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendAmountView.swift-25-25 (1)

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

Hardcoded "<symbol> <amount>" order ignores locale formatting.

Several locales/currencies place the symbol after the amount (e.g., 42,50 €, 42.50 kr) and use locale-specific spacing/grouping. Manually composing "\(currencySymbol) \(amount)" here (after stripping the symbol from the formatted string upstream) loses that information. If this view is reused for non‑USD locales, consider either:

  • passing a fully pre-formatted localized currency string and rendering it as-is, or
  • using Decimal.formatted(.currency(code:)) / a NumberFormatter with currency style inside the view.
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendAmountView.swift at line 25,
DashSpendAmountView currently composes currency display by concatenating
currencySymbol and amount with Text("\(currencySymbol) \(amount)"), which
ignores locale-specific symbol position and formatting; update the view to
either accept a fully preformatted localized currency string (e.g., pass
formattedAmount and render it directly) or perform locale-aware formatting
inside the view using the numeric value with Decimal.formatted(.currency(code:
currencyCode)) or a NumberFormatter configured with .currency style; modify the
view API and replace Text("\(currencySymbol) \(amount)") accordingly,
referencing DashSpendAmountView, currencySymbol, amount, and any
currencyCode/amount properties to locate and change the code.
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift-230-241 (1)

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

Dead empty if/else if branches inside the merchant-selection failure path.

When selectGiftCard returns nil, the loop at lines 234-239 iterates giftCards and runs empty if normalizedType == "fixed" / else if normalizedType == "range" branches before throwing. Both branches are no-ops, so this whole block can be deleted, or the original diagnostic logging restored behind #if DEBUG. As written it just adds cyclomatic complexity (the function already trips SwiftLint's cyclomatic_complexity at 17) without any runtime effect.

🧹 Proposed cleanup
             guard let selectedCard = PiggyCardsCache.shared.selectGiftCard(
                 from: giftCards,
                 forAmount: lineItem.denomination
             ) else {
-                for card in giftCards {
-                    let normalizedType = card.priceType.lowercased()
-                    if normalizedType == "fixed" {
-                    } else if normalizedType == "range" {
-                    }
-                }
                 throw DashSpendError.invalidAmount
             }

As per coding guidelines: Wrap debug print statements in #if DEBUG blocks or use conditional compilation to prevent performance impact from excessive logging.

🤖 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 `@DashWallet/Sources/Models/Explore`
Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift around lines 230 -
241, The empty conditional branches inside the failure path after attempting to
get selectedCard (the guard using PiggyCardsCache.shared.selectGiftCard with
giftCards and lineItem.denomination) should be removed or turned into debug-only
diagnostics: delete the for card in giftCards { let normalizedType =
card.priceType.lowercased(); if normalizedType == "fixed" { } else if
normalizedType == "range" { } } block to eliminate dead code and reduce
cyclomatic complexity, or replace its body with a debug-only log (wrap prints in
`#if` DEBUG) that records card.priceType or other diagnostic info before throwing
DashSpendError.invalidAmount.
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift-222-223 (1)

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

Dead empty loop body — drop it or restore the log behind #if DEBUG.

for (index, card) in giftCards.enumerated() { } does nothing; this looks like a stripped debug block. Per the repo guideline, Wrap debug print statements in #if DEBUG blocks or use conditional compilation to prevent performance impact from excessive logging., either delete the loop entirely or restore the intended DSLogger/print inside #if DEBUG.

🧹 Proposed cleanup
-        for (index, card) in giftCards.enumerated() {
-        }
-
         // Step 2: Select the appropriate gift cards for each line item

As per coding guidelines: Wrap debug print statements in #if DEBUG blocks or use conditional compilation to prevent performance impact from excessive logging.

🤖 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 `@DashWallet/Sources/Models/Explore`
Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift around lines 222 -
223, The empty loop for (index, card) in giftCards.enumerated() {} in
PiggyCardsRepository is dead code; either remove the entire loop or restore the
debug logging inside a conditional compilation block—e.g., move the intended
DSLogger/print call back into the loop and wrap it with `#if` DEBUG / `#endif`;
update the method that iterates giftCards so there are no no-op loops left
behind.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift-133-137 (1)

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

Fix the CTA localization key typo.

"Conttinue" will either render the typo directly or miss the existing "Continue" localization entry, so the keyboard action button can drift from the rest of the flow.

Suggested fix
             NumericKeyboardView(
                 value: $input,
                 showDecimalSeparator: true,
-                actionButtonText: NSLocalizedString("Conttinue", comment: ""),
+                actionButtonText: NSLocalizedString("Continue", comment: "DashSpend"),
                 actionEnabled: actionEnabled && !showLimits && hasValidLimits,
                 inProgress: inProgress,
                 actionHandler: onAction
             )
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift around lines 133 -
137, Replace the misspelled localization key passed to NumericKeyboardView's
actionButtonText parameter: change NSLocalizedString("Conttinue", comment: "")
to use the correct key NSLocalizedString("Continue", comment: "") (or the exact
existing localization key used elsewhere) so the CTA matches other screens;
update the string key wherever the actionButtonText is constructed for the
DashSpendSinglePanel and run localization checks to ensure the "Continue" entry
exists in Localizable.strings.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsInfoCard.swift-168-171 (1)

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

Add an accessibility label to the copy button.

This control is icon-only, so assistive technologies won't announce what gets copied here. The card number and PIN rows end up exposing two indistinguishable buttons.

Suggested fix
                 Button(action: onCopy) {
                     Icon(name: .custom("icon_copy_outline", maxHeight: 14))
                         .tint(.primaryText)
                 }
+                .accessibilityLabel(Text(title))
+                .accessibilityHint(Text(NSLocalizedString("Copies this value to the clipboard", comment: "DashSpend")))
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsInfoCard.swift
around lines 168 - 171, The copy Button in GiftCardDetailsInfoCard (the Button
using action: onCopy and Icon(name: .custom("icon_copy_outline"...))) is
icon-only and needs an accessibility label; update that Button to include a
clear .accessibilityLabel (e.g. "Copy card number" for the card number row and
"Copy PIN" for the PIN row) so VoiceOver can distinguish the two copy
controls—if the component is reused, accept or compute a descriptive label (prop
or local variable) and apply it to the Button that wraps the Icon.
🧹 Nitpick comments (9)
DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift (1)

132-329: ⚡ Quick win

Gate this helper behind PIGGYCARDS_ENABLED too.

This new path is PiggyCards-specific end-to-end, but it currently compiles for every DEBUG/Testflight build. Please wrap the whole block with the PiggyCards feature flag as well so non-PiggyCards variants do not carry provider-specific test data logic.

As per coding guidelines: "Use conditional compilation #if PIGGYCARDS_ENABLED for PiggyCards provider support in Swift code."

🤖 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 `@DashWallet/Sources/Models/Explore` Dash/Infrastructure/Database
Connection/ExploreDatabaseConnection.swift around lines 132 - 329, The
PiggyCards-specific test merchant helpers (addTestMerchantAfterSync,
insertTestMerchant, piggyCardsTestMerchants, addPiggyCardsTestMerchants) are
only gated by DEBUG/Testflight and must also be compiled only when
PIGGYCARDS_ENABLED is set; wrap the entire block that defines
addTestMerchantAfterSync, insertTestMerchant, the PiggyCardsTestMerchant struct,
piggyCardsTestMerchants array, and addPiggyCardsTestMerchants function with a
conditional compilation check `#if` PIGGYCARDS_ENABLED (in addition to the
existing DEBUG/Testflight check) so the provider-specific code is excluded for
builds without PiggyCards support.
DashWallet/Sources/UI/SwiftUI Components/DashStepper.swift (1)

55-65: 💤 Low value

Misleading parameter name systemImage — actually loads from the asset catalog

Image(systemImage) calls Image(_ name: String), which looks up the asset catalog. The name systemImage strongly implies Image(systemName:) (SF Symbols). Since custom plus/minus assets were added to the catalog, the behavior is correct, but the parameter name will mislead future maintainers into thinking SF Symbol names are expected.

♻️ Rename to `imageName`
-    private func stepperButton(
-        systemImage: String,
+    private func stepperButton(
+        imageName: String,
         enabled: Bool,
         accessibilityLabel: String,
         action: `@escaping` () -> Void
     ) -> some View {
-        Image(systemImage)
+        Image(imageName)

Update the two call sites accordingly ("minus" and "plus").

🤖 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 `@DashWallet/Sources/UI/SwiftUI` Components/DashStepper.swift around lines 55 -
65, Rename the misleading parameter systemImage in the stepperButton function to
imageName (function:
stepperButton(systemImage:enabled:accessibilityLabel:action:)) and update its
two call sites that pass "minus" and "plus" to use the new name; keep the
Image(initializer) usage as-is (Image(imageName)) since these are asset-catalog
images rather than SF Symbols. Ensure the function signature and all references
(including any internal uses like Image(...) and accessibilityLabel arguments)
are updated to the new identifier.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift (2)

19-20: 💤 Low value

Sort imports (lint).

SwiftLint warns that import SDWebImageSwiftUI is out of order; place SDWebImageSwiftUI before SwiftUI (alphabetical) to clear the warning.

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendPayIntro.swift around lines 19 - 20,
The import ordering is triggering a SwiftLint warning because imports must be
alphabetical; swap the two import lines so that "import SDWebImageSwiftUI"
appears before "import SwiftUI" (i.e., reorder the top-level imports in
DashSpendPayIntro.swift) to satisfy the linter.

94-96: 💤 Low value

Use the explicit label: form (lint).

SwiftLint flags this as multiple_closures_with_trailing_closure. The explicit form also reads better when the action closure does non-trivial work like wrapping in withAnimation.

♻️ Proposed fix
-                Button(action: { withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() } }) {
-                    eyeIcon
-                }
+                Button {
+                    withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() }
+                } label: {
+                    eyeIcon
+                }
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendPayIntro.swift around lines 94 - 96,
Replace the trailing-closure Button usage with the explicit label: form to
satisfy lint and improve clarity: keep the action closure that toggles
balanceHidden inside withAnimation (the existing Button(action: {
withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() } }) usage)
but pass the view for the button content via the label: parameter (using eyeIcon
as the label). This changes the call to use the Button(action: { ... }, label: {
eyeIcon }) initializer so SwiftLint no longer flags
multiple_closures_with_trailing_closure and the action/label intent is explicit.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift (2)

73-101: 💤 Low value

Use the Button { } label: { } form (lint).

SwiftLint flags line 75 with multiple_closures_with_trailing_closure. Switching to the explicit label: form silences the warning and reads more clearly with this much label content.

♻️ Proposed fix
-                Button(action: {
-                    onSelectCard(index)
-                }) {
+                Button {
+                    onSelectCard(index)
+                } label: {
                     HStack(spacing: 20) {
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift
around lines 73 - 101, Replace the current Button call that uses two trailing
closures (Button(action: { onSelectCard(index) }) { ... }) with the explicit
label form to satisfy SwiftLint: use the initializer Button {
onSelectCard(index) } label: { /* preserve the HStack content including
Icon(...) and Text(card.formattedPrice) */ }, keeping the same modifiers
(.buttonStyle(.plain), .contentShape(Rectangle()), .frame(height: 42)) and
references to onSelectCard, Icon(name:), card.formattedPrice, and index so
behavior and layout remain identical while removing the
multiple_closures_with_trailing_closure lint warning.

72-72: ⚡ Quick win

Prefer a stable identity over \.offset in ForEach.

Array(cards.enumerated()) keyed by \.offset ties identity to position. If the cards array is ever reordered, partially refreshed, or appended-to mid-render, SwiftUI will reuse rows incorrectly and may show stale animations or selection state. GiftCardDetailsCardItem already exposes id (see preview values), so prefer something like:

♻️ Proposed fix
-            ForEach(Array(cards.enumerated()), id: \.offset) { index, card in
+            ForEach(Array(cards.enumerated()), id: \.element.id) { index, card in
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift
at line 72, The ForEach is using Array(cards.enumerated()) with id: \.offset
which ties identity to array position; change the iteration to use the card's
stable identifier (e.g., use cards directly in ForEach and set id: \.id or rely
on the card conforming to Identifiable) so rows keep stable identities — update
the ForEach line that currently references Array(cards.enumerated()) and id:
\.offset to iterate over cards and use card.id (GiftCardDetailsCardItem /
GiftCardPurchaseSelectionSheet ForEach) to ensure stable identities.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift (1)

42-50: 💤 Low value

Double .frame is a confusing way to express padding around the badge.

Lines 46–47 apply two consecutive frames (20×20 then 23×23) to size the icon and then leave room for the circular background. This works but obscures intent. A clearer expression is sizing the icon once and using .padding(1.5) (or a fixed frame with .background(Circle()...) alone) so the next reader doesn't have to re-derive the geometry.

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift
around lines 42 - 50, Replace the confusing double .frame usage in
GiftCardDetailsMerchantHeader (inside the if merchantIcon != nil block) by
sizing the Image once (e.g., .frame(width: 20, height: 20) on the Image) and
then use .padding(1.5) (or a single larger frame) before applying
.background(Circle().fill(Color.secondaryBackground)) and .offset(x:2,y:2); this
keeps the intent clear by explicitly giving the icon size and the surrounding
badge padding rather than chaining two frames.
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift (1)

207-328: ⚖️ Poor tradeoff

Consider splitting orderGiftCards to lower its cyclomatic complexity.

After deleting the dead-log blocks above, this method still does input validation, cache lookup, per-line-item card selection, basket summary computation, request building, request firing, and multi-arm error mapping — hitting SwiftLint's cyclomatic_complexity: 17. Extracting selectOrders(forLineItems:in:) and buildOrderRequest(orders:email:) helpers would bring the body back under the threshold and make the multi-line-item flow easier to test.

🤖 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 `@DashWallet/Sources/Models/Explore`
Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift around lines 207 -
328, The orderGiftCards method is too complex; extract the per-line-item
selection and the request-building into helpers to reduce cyclomatic complexity.
Create a function selectOrders(forLineItems: [OrderLineItem], in giftCards:
[PiggyCard]) -> (orders: [PiggyCardsOrder], basketItems: [SelectedBasketItem])
that encapsulates the loop that validates quantities, calls
PiggyCardsCache.selectGiftCard, handles fixed/range type checks and throws
DashSpendError.invalidAmount as needed; and create buildOrderRequest(orders:
[PiggyCardsOrder], recipientEmail: String) -> PiggyCardsOrderRequest that builds
PiggyCardsUserMetadata, PiggyCardsUser and the PiggyCardsOrderRequest
(preserving the Android hardcoded values). Remove the dead log blocks from
orderGiftCards and have orderGiftCards call these two helpers, compute
summaryDiscountPercentage from returned basketItems, then continue with the
network call and existing error handling unchanged.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsPoweredBySection.swift (1)

29-39: ⚡ Quick win

Use typed GiftCardProvider enum instead of hardcoded provider strings.

The file currently branches on provider == "PiggyCards", but GiftCardProvider enum exists with properly defined cases (.ctx, .piggyCards) and computed properties (displayName, logoName). Refactoring provider from String? to GiftCardProvider? would eliminate fragile string matching. Note: this requires updating the type throughout the view hierarchy (GiftCardDetailsViewModelGiftCardDetailsViewGiftCardPurchaseSelectionSheetGiftCardDetailsPoweredBySection), so plan for medium effort across multiple files. The enum's built-in properties would also simplify logo selection logic.

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsPoweredBySection.swift
around lines 29 - 39, Replace fragile string matching by changing provider from
String? to the typed GiftCardProvider? and propagate that type change through
GiftCardDetailsViewModel, GiftCardDetailsView, GiftCardPurchaseSelectionSheet,
and GiftCardDetailsPoweredBySection; update constructors/initializers and stored
properties to accept GiftCardProvider? and adjust call sites accordingly; inside
GiftCardDetailsPoweredBySection use provider?.logoName and provider?.displayName
(or switch on GiftCardProvider cases .ctx/.piggyCards) to choose the Image and
frame parameters instead of comparing provider == "PiggyCards", ensuring you use
the enum’s computed properties for logo selection and sizing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d01118b-0536-4079-aee5-6683af9319d3

📥 Commits

Reviewing files that changed from the base of the PR and between d64385a and 81e7afb.

⛔ Files ignored due to path filters (15)
  • DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x1.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x2.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/eye_closed-icon@x3.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x1.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x2.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/eye_opened-icon@x3.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x1.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x2.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/icon-gift_card-piggy_cards@x3.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x1.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x2.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/minus.imageset/minus-icon@x3.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x1.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x2.png is excluded by !**/*.png
  • DashWallet/Resources/AppAssets.xcassets/plus.imageset/plus-icon@x3.png is excluded by !**/*.png
📒 Files selected for processing (44)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Resources/AppAssets.xcassets/eye_closed-icon.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/eye_opened-icon.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/icon-gift_card-piggy_cards.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/minus.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/plus.imageset/Contents.json
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
  • DashWallet/Sources/Models/Explore Dash/Model/DashSpend/PiggyCardsConstants.swift
  • DashWallet/Sources/Models/Explore Dash/Model/Entites/CTXSpendModels.swift
  • DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendAmountView.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendDenominationRow.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFixedContent.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFlexibleContent.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendConfirmationDialog.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsHowToUseSection.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsInfoCard.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsPoweredBySection.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsView.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/UI/Home/HomeViewController.swift
  • DashWallet/Sources/UI/Home/Views/HomeView.swift
  • DashWallet/Sources/UI/Main/MainTabbarController.swift
  • DashWallet/Sources/UI/SwiftUI Components/BottomSheet.swift
  • DashWallet/Sources/UI/SwiftUI Components/Button.swift
  • DashWallet/Sources/UI/SwiftUI Components/Color+DWStyle.swift
  • DashWallet/Sources/UI/SwiftUI Components/DashStepper.swift
  • DashWallet/Sources/UI/SwiftUI Components/NumericKeyboardView.swift
  • DashWallet/Sources/UI/SwiftUI Components/SegmentedControl.swift
  • DashWallet/en.lproj/Localizable.strings
  • Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha40.colorset/Contents.json
  • Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha5.colorset/Contents.json
  • Shared/Resources/SharedAssets.xcassets/Colors/Black/Black1000Alpha8.colorset/Contents.json
  • Shared/Resources/SharedAssets.xcassets/Colors/Gray/Gray300Alpha20.colorset/Contents.json
💤 Files with no reviewable changes (2)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift

Comment thread DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift Outdated
Comment thread DashWallet/Sources/UI/Home/Views/HomeView.swift
Comment thread DashWallet/Sources/UI/Home/Views/HomeView.swift
Comment thread DashWallet/Sources/UI/Home/Views/HomeView.swift
@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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 (1)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (1)

308-317: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply .rounded() before casting Double to UInt64 to prevent floating-point precision loss in satoshi conversion.

giftCardInfo.amount is Double. When multiplied by 100,000,000 and cast directly to UInt64, binary rounding artifacts (e.g., 0.123456 represented as 0.12345599…) can silently lose one satoshi from the payment amount. This file already applies .rounded() before similar type casts (line 739). Use .rounded() here to ensure the on-chain amount matches the quoted DASH price.

🛡️ Proposed fix
             // Convert DASH amount to satoshis (1 DASH = 100,000,000 satoshis)
-            let dashAmountInSatoshis = UInt64(giftCardInfo.amount * 100_000_000)
+            let dashAmountInSatoshis = UInt64((giftCardInfo.amount * 100_000_000).rounded())
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift around lines 308 - 317, The
calculation of dashAmountInSatoshis should apply .rounded() before casting to
UInt64 to avoid floating-point precision loss: change the expression that
creates dashAmountInSatoshis from UInt64(giftCardInfo.amount * 100_000_000) to
use giftCardInfo.amount.rounded() (i.e., UInt64(giftCardInfo.amount.rounded() *
100_000_000)) so the satoshi amount passed into
sendCoinsService.sendCoins(address:amount:) matches the quoted DASH price;
update the assignment where dashAmountInSatoshis is defined in
DashSpendPayViewModel to use .rounded() on giftCardInfo.amount.
♻️ Duplicate comments (1)
DashWallet/Sources/UI/Home/Views/HomeView.swift (1)

552-557: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

onChange(selectedCardIndex) may overwrite the explicit showBackButton = true from onSelectCard.

When the user picks a card at line 519-523, three state mutations happen in the same render: selectedCardIndex = index, showBackButton = true, and prefersLargeDetent = true. After the state batch commits, onChange(of: selectedCardIndex) fires and reassigns showBackButton = shouldShowBackButton, which evaluates to false because line 626-628 still only returns txDetailRoute != nil. The same happens on onAppear (line 546) when re-entering with a previously selected card. The flow currently works only if GiftCardDetailsView re-asserts the back button via onShowBackButton(true) on its onAppear; otherwise the back button briefly disappears (or fully disappears) and the multi-card "return to picker" branch in handleBackNavigation becomes unreachable.

The cleanest fix is the one suggested in the prior review — make shouldShowBackButton aware of the selection state. This was marked as addressed in commit 80f81bd, but the predicate at line 626-628 still only checks txDetailRoute.

🛡️ Suggested fix
     private var shouldShowBackButton: Bool {
-        txDetailRoute != nil
+        txDetailRoute != nil || (selectedCardIndex != nil && viewModel.uiState.cards.count > 1)
     }
🤖 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 `@DashWallet/Sources/UI/Home/Views/HomeView.swift` around lines 552 - 557, The
onChange handler for selectedCardIndex overwrites an explicit showBackButton set
by onSelectCard because shouldShowBackButton currently only checks
txDetailRoute; update the predicate used by shouldShowBackButton (the function
or computed Bool referenced at its declaration and used in the onChange block)
to also return true when selectedCardIndex != nil (or when a card is selected),
so that onChange(of: selectedCardIndex) doing showBackButton =
shouldShowBackButton preserves the intent; locate and modify the
shouldShowBackButton computed property / function and any callers (e.g., in
onChange, onAppear) to include selectedCardIndex != nil in its logic so the back
button remains visible after selection.
🧹 Nitpick comments (5)
DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift (1)

109-109: ⚡ Quick win

Add emoji log prefix for DSLogger consistency

Line 109 should use the repo’s emoji-prefixed logging format to keep Explore logs filterable.

Suggested change
-                DSLogger.log("ExploreDash: local explore.db missing, forcing cloud database download")
+                DSLogger.log("💾 ExploreDash: local explore.db missing, forcing cloud database download")

As per coding guidelines: "Use emoji debug markers for easy log filtering: 🎯 🌐 💾 🎨 🔍 📍 🗺️ (e.g., DSLogger.log("🔍 Message:"))."

🤖 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 `@DashWallet/Sources/Models/Explore`
Dash/Services/ExploreDatabaseSyncManager.swift at line 109, Update the log call
in ExploreDatabaseSyncManager so it uses the repo's emoji-prefixed format;
replace the existing DSLogger.log("ExploreDash: local explore.db missing,
forcing cloud database download") invocation with a message that includes the
emoji prefix (e.g., "🔍 ExploreDash: local explore.db missing, forcing cloud
database download") so logs remain filterable and consistent with DSLogger.log
usage across the project.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift (1)

94-96: 💤 Low value

Avoid trailing closure when passing both action: and label:.

SwiftLint flags multiple_closures_with_trailing_closure here. Switching to the explicit Button { } label: { } form is the idiomatic fix and silences the warning.

♻️ Proposed refactor
-                Button(action: { withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() } }) {
-                    eyeIcon
-                }
+                Button {
+                    withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() }
+                } label: {
+                    eyeIcon
+                }
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendPayIntro.swift around lines 94 - 96,
Replace the trailing-closure Button usage to the explicit SwiftUI syntax to
satisfy SwiftLint: change the Button call that currently uses Button(action: {
withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() } }) { eyeIcon
} to the idiomatic form using a leading action closure and a labeled label
closure (i.e., Button { withAnimation(.easeInOut(duration: 0.2)) {
balanceHidden.toggle() } } label: { eyeIcon }), updating the instance in
DashSpendPayIntro (the Button referencing balanceHidden and eyeIcon) so
SwiftLint no longer reports multiple_closures_with_trailing_closure.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift (1)

54-63: ⚡ Quick win

Use the new Black1000Alpha8 design token instead of a hardcoded RGB literal.

This PR adds Black1000Alpha8 as a design token. Replacing the inline Color(red: 10/255, green: 11/255, blue: 13/255).opacity(0.7) with the token keeps the chip consistent with the design system and resolves the SwiftLint operator_usage_whitespace warnings on this line.

🎨 Proposed refactor
                 if let warning = inventoryWarning {
                     Text(warning)
                         .font(.caption)
                         .foregroundColor(.white)
                         .padding(.horizontal, 12)
                         .padding(.vertical, 8)
-                        .background(Color(red: 10/255, green: 11/255, blue: 13/255).opacity(0.7))
+                        .background(Color.black1000Alpha8)
                         .cornerRadius(10)
                         .offset(y: 20)
                 }
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift around lines 54 -
63, Replace the hardcoded background color in the inventory warning chip with
the new design token Black1000Alpha8: locate the conditional that renders
Text(warning) in DashSpendMultiplePanel (the inventoryWarning block) and swap
Color(red: 10/255, green: 11/255, blue: 13/255).opacity(0.7) for the project
design token (e.g. use the token API available in your codebase such as
Color.DesignTokens.Black1000Alpha8 or Color("Black1000Alpha8") depending on how
tokens are exposed) so the chip uses Black1000Alpha8 and resolves the SwiftLint
operator_usage_whitespace warnings.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift (2)

78-84: 💤 Low value

Index badges only render for 3+ cards — duplicate-priced 2-card case has no disambiguator.

cards.count > 2 hides the numeric badge whenever there are exactly two cards. If both denominations share formattedPrice (a plausible result of two providers/sources for the same amount), the rows become visually identical and the user cannot tell which is which until selection. Consider cards.count > 1 (always show a 1/2 badge when more than one card), or add another distinguishing field (provider/source/sourceId) to the row.

♻️ Suggested adjustment
-                            if cards.count > 2 {
+                            if cards.count > 1 {
                                 Text("\(index + 1)")
                                     .font(.caption1.weight(.medium))
                                     .padding(.horizontal, 4)
                                     .background(Color.gray50)
                                     .clipShape(.rect(cornerRadius: 5))
                             }
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift
around lines 78 - 84, The numeric index badge is hidden by the condition
cards.count > 2 which leaves two-item lists ambiguous when both cards share the
same formattedPrice; update the condition to cards.count > 1 so the
Text("\(index + 1)") badge is shown whenever there is more than one card, or
alternatively augment the row to display an additional distinguishing field
(e.g., provider/source or sourceId) alongside formattedPrice; locate the badge
rendering in GiftCardPurchaseSelectionSheet.swift (the block containing
cards.count > 2 and Text("\(index + 1)")) and replace the condition or add the
extra identifier display so duplicate-priced two-card rows are disambiguated.

86-91: ⚖️ Poor tradeoff

Magic string "PiggyCards" repeated across views.

provider is a String? and "PiggyCards" literals are used in this file and in GiftCardDetailsPoweredBySection/HomeView. As multi-provider support grows, a typed enum (or static constant on the existing provider model) would prevent typos and make refactors straightforward.

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift
around lines 86 - 91, The code is using the magic string "PiggyCards" to test
provider (provider: String?), which is error-prone; introduce a typed Provider
enum (e.g., enum GiftCardProvider { case piggyCards, other(...) } or a static
constant on the provider model) and replace string comparisons with enum checks:
add an initializer or mapper from String? to GiftCardProvider? (preserving nil)
and update usages in GiftCardPurchaseSelectionSheet (Icon selection),
GiftCardDetailsPoweredBySection, and HomeView to compare against
GiftCardProvider.piggyCards (or the static constant) instead of the literal
"PiggyCards". Ensure the Icon selection uses the enum value (providerEnum ==
.piggyCards) and keep backward-compatible mapping for existing provider strings.
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift:
- Line 136: Fix the typo in the DashSpendSinglePanel localization lookup by
replacing the misspelled key "Conttinue" with "Continue" where actionButtonText
is set (look for actionButtonText: NSLocalizedString("Conttinue", comment: "")
in DashSpendSinglePanel.swift) so it matches the other panels (e.g.,
DashSpendMultiplePanel) and avoids a duplicate/orphan localization entry.

In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift:
- Around line 444-451: The denominations mapping drops decimal-formatted strings
because it uses Int($0); replace the compactMap closure in the newDenominations
assignment so it calls parseWholeDollarDenomination($0) instead (e.g.,
merchantInfo.denominations.compactMap { parseWholeDollarDenomination($0) }) to
correctly parse values like "5.00" or "10.0" and produce whole-dollar Ints; the
helper function parseWholeDollarDenomination(_:) is defined in this file and
should be used to populate self.denominations.

---

Outside diff comments:
In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift:
- Around line 308-317: The calculation of dashAmountInSatoshis should apply
.rounded() before casting to UInt64 to avoid floating-point precision loss:
change the expression that creates dashAmountInSatoshis from
UInt64(giftCardInfo.amount * 100_000_000) to use giftCardInfo.amount.rounded()
(i.e., UInt64(giftCardInfo.amount.rounded() * 100_000_000)) so the satoshi
amount passed into sendCoinsService.sendCoins(address:amount:) matches the
quoted DASH price; update the assignment where dashAmountInSatoshis is defined
in DashSpendPayViewModel to use .rounded() on giftCardInfo.amount.

---

Duplicate comments:
In `@DashWallet/Sources/UI/Home/Views/HomeView.swift`:
- Around line 552-557: The onChange handler for selectedCardIndex overwrites an
explicit showBackButton set by onSelectCard because shouldShowBackButton
currently only checks txDetailRoute; update the predicate used by
shouldShowBackButton (the function or computed Bool referenced at its
declaration and used in the onChange block) to also return true when
selectedCardIndex != nil (or when a card is selected), so that onChange(of:
selectedCardIndex) doing showBackButton = shouldShowBackButton preserves the
intent; locate and modify the shouldShowBackButton computed property / function
and any callers (e.g., in onChange, onAppear) to include selectedCardIndex !=
nil in its logic so the back button remains visible after selection.

---

Nitpick comments:
In `@DashWallet/Sources/Models/Explore`
Dash/Services/ExploreDatabaseSyncManager.swift:
- Line 109: Update the log call in ExploreDatabaseSyncManager so it uses the
repo's emoji-prefixed format; replace the existing DSLogger.log("ExploreDash:
local explore.db missing, forcing cloud database download") invocation with a
message that includes the emoji prefix (e.g., "🔍 ExploreDash: local explore.db
missing, forcing cloud database download") so logs remain filterable and
consistent with DSLogger.log usage across the project.

In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift:
- Around line 54-63: Replace the hardcoded background color in the inventory
warning chip with the new design token Black1000Alpha8: locate the conditional
that renders Text(warning) in DashSpendMultiplePanel (the inventoryWarning
block) and swap Color(red: 10/255, green: 11/255, blue: 13/255).opacity(0.7) for
the project design token (e.g. use the token API available in your codebase such
as Color.DesignTokens.Black1000Alpha8 or Color("Black1000Alpha8") depending on
how tokens are exposed) so the chip uses Black1000Alpha8 and resolves the
SwiftLint operator_usage_whitespace warnings.

In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendPayIntro.swift:
- Around line 94-96: Replace the trailing-closure Button usage to the explicit
SwiftUI syntax to satisfy SwiftLint: change the Button call that currently uses
Button(action: { withAnimation(.easeInOut(duration: 0.2)) {
balanceHidden.toggle() } }) { eyeIcon } to the idiomatic form using a leading
action closure and a labeled label closure (i.e., Button {
withAnimation(.easeInOut(duration: 0.2)) { balanceHidden.toggle() } } label: {
eyeIcon }), updating the instance in DashSpendPayIntro (the Button referencing
balanceHidden and eyeIcon) so SwiftLint no longer reports
multiple_closures_with_trailing_closure.

In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift:
- Around line 78-84: The numeric index badge is hidden by the condition
cards.count > 2 which leaves two-item lists ambiguous when both cards share the
same formattedPrice; update the condition to cards.count > 1 so the
Text("\(index + 1)") badge is shown whenever there is more than one card, or
alternatively augment the row to display an additional distinguishing field
(e.g., provider/source or sourceId) alongside formattedPrice; locate the badge
rendering in GiftCardPurchaseSelectionSheet.swift (the block containing
cards.count > 2 and Text("\(index + 1)")) and replace the condition or add the
extra identifier display so duplicate-priced two-card rows are disambiguated.
- Around line 86-91: The code is using the magic string "PiggyCards" to test
provider (provider: String?), which is error-prone; introduce a typed Provider
enum (e.g., enum GiftCardProvider { case piggyCards, other(...) } or a static
constant on the provider model) and replace string comparisons with enum checks:
add an initializer or mapper from String? to GiftCardProvider? (preserving nil)
and update usages in GiftCardPurchaseSelectionSheet (Icon selection),
GiftCardDetailsPoweredBySection, and HomeView to compare against
GiftCardProvider.piggyCards (or the static constant) instead of the literal
"PiggyCards". Ensure the Icon selection uses the enum value (providerEnum ==
.piggyCards) and keep backward-compatible mapping for existing provider strings.
🪄 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: 9cb76500-9eab-4cf8-9230-73d45d0d889d

📥 Commits

Reviewing files that changed from the base of the PR and between 81e7afb and d57dc1c.

📒 Files selected for processing (13)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFixedContent.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFlexibleContent.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendMultiplePanel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendPayIntro.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardPurchaseSelectionSheet.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/UI/Home/Views/HomeView.swift
  • DashWallet/Sources/UI/SwiftUI Components/NumericKeyboardView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetails/Components/GiftCardDetailsMerchantHeader.swift

Comment thread DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift Outdated
@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (1)

309-316: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use rounding when converting PiggyCards payment amount from DASH to satoshis.

GiftCardInfo.amount is a Double. When you compute UInt64(giftCardInfo.amount * 100_000_000), the multiplication produces a Double result that is then truncated toward zero on the integer cast. For example, 0.29 becomes 28_999_999.999... and truncates to 28_999_999 satoshis, underpaying by one satoshi—enough to fail the transaction.

Round the result before casting to UInt64, or use Decimal throughout this conversion path to preserve precision.

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift around lines 309 - 316, The
conversion multiplying GiftCardInfo.amount (a Double) by 100_000_000 and casting
directly to UInt64 truncates fractional satoshis and can underpay (e.g., 0.29 →
28_999_999); update the calculation that sets dashAmountInSatoshis so it rounds
the Double result before casting (e.g., use round(...) or construct via Decimal
and NSDecimalNumber/Decimal rounding) to ensure exact satoshi amounts, then pass
that rounded UInt64 into sendCoinsService.sendCoins(address:
giftCardInfo.paymentAddress, amount: dashAmountInSatoshis) so PIN authorization
flow remains unchanged.
🧹 Nitpick comments (5)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift (4)

64-64: ⚡ Quick win

Move @ViewBuilder attributes to their own lines.

SwiftLint flags these @ViewBuilder attributes as they should be on separate lines per the project's style configuration.

🎨 Proposed fix
+    `@ViewBuilder`
     private var costMessageView: some View {
-    `@ViewBuilder`
     private var costMessageView: some View {

+    `@ViewBuilder`
     private var limitsView: some View {
-    `@ViewBuilder`
     private var limitsView: some View {

Also applies to: 76-76

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift at line 64, SwiftLint
wants the `@ViewBuilder` attributes on their own lines for the view properties;
locate the property declarations for costMessageView and the other similar
property (the one starting around line 76) and move the `@ViewBuilder` attribute
so it sits on its own line immediately above the property declaration (e.g.,
place "@ViewBuilder" on a separate line before "private var costMessageView:
some View" and do the same for the other annotated view property) to satisfy the
style rule.

136-136: ⚡ Quick win

Add descriptive comment to localization string.

The localization comment is empty. Add a descriptive comment like "DashSpend" to help translators understand the context, consistent with other localized strings in this PR (e.g., line 102).

📝 Proposed fix
-                actionButtonText: NSLocalizedString("Continue", comment: ""),
+                actionButtonText: NSLocalizedString("Continue", comment: "DashSpend"),
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift at line 136, The
NSLocalizedString call used for actionButtonText in DashSpendSinglePanel.swift
has an empty comment; update the comment argument to a descriptive context
(e.g., "DashSpend") so translators understand the usage. Locate the
actionButtonText: NSLocalizedString("Continue", comment: "") and replace the
empty comment with a meaningful string matching other entries in this PR (for
example "DashSpend") to keep comments consistent.

25-25: ⚡ Quick win

Remove commented-out code.

Commented-out code should be removed before merging. If this padding was intentionally removed, delete the line. If it needs to be reconsidered later, document the reason in a TODO comment or track it in an issue.

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift at line 25, In
DashSpendSinglePanel.swift remove the leftover commented-out layout line "//    
.padding(.bottom, 30)" from the view body; if the padding change was intentional
and needs tracking, replace the commented line with a concise TODO comment
referencing the decision or link to an issue ID instead of leaving commented
code (locate the commented line near the DashSpendSinglePanel view declaration
and update accordingly).

214-214: ⚡ Quick win

Remove redundant nil initialization.

SwiftLint flags this optional initialization. Optionals are implicitly nil in Swift, so the explicit = nil is redundant.

🎨 Proposed fix
-    `@State` private var selected: Int? = nil
+    `@State` private var selected: Int?
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift at line 214, The
`@State` variable selected is explicitly initialized to nil which is redundant;
update the declaration of selected (the `@State` private var selected: Int?
property in DashSpendSinglePanel) to remove the "= nil" so it becomes simply
`@State` private var selected: Int? to satisfy SwiftLint and rely on Swift's
implicit nil initialization for optionals.
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (1)

414-613: 🏗️ Heavy lift

Split updateMerchantInfo() into provider-specific normalization helpers.

This method is now doing fetch orchestration, DTO parsing, PiggyCards denomination reconciliation, and UI-state publication in one place, which is why it already trips the complexity gate. Extract the CTX and PiggyCards normalization into helper/service methods that return a single normalized state object, then apply that state on the main actor here. That will make this flow much easier to reason about and much safer to change. As per coding guidelines, "Use service layer with dedicated services for major functionality (CoinJoin, networking, database)."

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift around lines 414 - 613,
Refactor updateMerchantInfo() by extracting provider-specific normalization into
two helper methods (e.g.,
normalizeCTXMerchant(merchantInfo:)->DashSpendNormalizedState and
normalizePiggyCardsState(giftCards:sourceId:)->DashSpendNormalizedState) so the
method only orchestrates fetches and applies a single normalized state on the
MainActor; move all DTO parsing and reconciliation logic currently using
ctxSpendRepository.getMerchant, PiggyCardsRepository.getGiftCards,
parseWholeDollarDenomination, providerConfiguredIsFixed, denomDiscounts,
denomInventory, rangeMin/rangeMax, etc. into those helpers (they should compute
isFixedDenomination, minimumAmount, maximumAmount, denominations,
savingsFraction, denominationDiscounts and denominationInventory), then call
await MainActor.run once to set self.* from the returned
DashSpendNormalizedState and finish with the existing
isLoading/objectWillChange/checkAmountForErrors flow.
🤖 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 `@DashWallet/Sources/Models/Transactions/SendCoinsService.swift`:
- Around line 99-107: In payWithDashUrl, authentication is performed before
validating the payment URL/request, causing unnecessary biometric/PIN prompts
for malformed links; move the URL parsing and payment request validation (the
guard for URL(string: paymentUrlString) and the subsequent payment request
creation/validation code around lines 115–117) to occur before calling
authenticateForPayment(), and only call authenticateForPayment() when the
paymentUrl and payment request are successfully validated; update any related
error throws (e.g., DashSpendError.paymentProcessingError("Invalid payment
URL")) to be returned prior to invoking authenticateForPayment().
- Around line 28-36: authenticateForPayment() currently returns a Bool and
collapses "failed auth" and "user cancelled" into false, causing callers to
always treat failures as "Authentication cancelled"; change
authenticateForPayment (and the similar calls around the other occurrences) to
surface the distinct outcome by returning an enum (e.g., AuthenticationOutcome {
case success, cancelled, failed }) or by throwing specific errors (e.g.,
AuthenticationCancelledError vs AuthenticationFailedError) instead of Bool,
update the continuation to resume with the appropriate enum/call throw for
DSAuthenticationManager.sharedInstance()'s authenticatedOrSuccess and cancelled
values, and then update the callers in this file to handle/propagate the
distinct cases (throwing AuthenticationCancelledError only when cancelled is
true, and a different error when auth failed).

In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift:
- Line 143: The .background(ignoresSafeAreaEdges: .top) modifier usage in
DashSpendSinglePanel is invalid because it omits the required content parameter;
update the modifier on the view inside the DashSpendSinglePanel (where
.background(ignoresSafeAreaEdges: .top) is applied) to either call
.ignoresSafeArea(edges: .top) to simply ignore the top safe area, or supply a
background view/color as the first parameter to
.background(_:ignoresSafeAreaEdges:) if you intend to set a background; adjust
the modifier placement on the view so the corrected modifier replaces the broken
.background call.

In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift:
- Around line 93-96: updateTotalAmount(_:) only sets amount, leaving
savingsFraction stale and costMessage wrong for mixed-denomination baskets;
modify updateTotalAmount(_:) to accept or access the current selected
basket/quantities (e.g., selectedQuantities or basket) and recompute the
checkout total and savingsFraction from that basket before updating amount and
calling checkAmountForErrors(); in practice either add a parameter like
updateTotalAmount(_ total: Decimal, selectedQuantities: [Denom: Int]) or call an
existing helper (implement computeCheckoutTotal(from:),
computeSavingsFraction(for:)) inside updateTotalAmount(_:), set amount to the
recomputed checkout total, update savingsFraction, then refresh costMessage and
call checkAmountForErrors().
- Around line 446-451: After updating denominations in the MainActor.run block,
clear any stale selection by resetting selectedDenomination (and any related
selection state used by showCost) so a previously cached choice from local
merchant data cannot persist when the CTX response provides a new fixed list;
mirror the PiggyCards reset logic by setting selectedDenomination = nil (and if
present, selectedDenominationIndex or similar selection properties) immediately
after assigning self.denominations so UI logic (showCost) treats no selection as
current until the user picks a valid new denomination.

---

Outside diff comments:
In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift:
- Around line 309-316: The conversion multiplying GiftCardInfo.amount (a Double)
by 100_000_000 and casting directly to UInt64 truncates fractional satoshis and
can underpay (e.g., 0.29 → 28_999_999); update the calculation that sets
dashAmountInSatoshis so it rounds the Double result before casting (e.g., use
round(...) or construct via Decimal and NSDecimalNumber/Decimal rounding) to
ensure exact satoshi amounts, then pass that rounded UInt64 into
sendCoinsService.sendCoins(address: giftCardInfo.paymentAddress, amount:
dashAmountInSatoshis) so PIN authorization flow remains unchanged.

---

Nitpick comments:
In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift:
- Line 64: SwiftLint wants the `@ViewBuilder` attributes on their own lines for
the view properties; locate the property declarations for costMessageView and
the other similar property (the one starting around line 76) and move the
`@ViewBuilder` attribute so it sits on its own line immediately above the property
declaration (e.g., place "@ViewBuilder" on a separate line before "private var
costMessageView: some View" and do the same for the other annotated view
property) to satisfy the style rule.
- Line 136: The NSLocalizedString call used for actionButtonText in
DashSpendSinglePanel.swift has an empty comment; update the comment argument to
a descriptive context (e.g., "DashSpend") so translators understand the usage.
Locate the actionButtonText: NSLocalizedString("Continue", comment: "") and
replace the empty comment with a meaningful string matching other entries in
this PR (for example "DashSpend") to keep comments consistent.
- Line 25: In DashSpendSinglePanel.swift remove the leftover commented-out
layout line "//            .padding(.bottom, 30)" from the view body; if the
padding change was intentional and needs tracking, replace the commented line
with a concise TODO comment referencing the decision or link to an issue ID
instead of leaving commented code (locate the commented line near the
DashSpendSinglePanel view declaration and update accordingly).
- Line 214: The `@State` variable selected is explicitly initialized to nil which
is redundant; update the declaration of selected (the `@State` private var
selected: Int? property in DashSpendSinglePanel) to remove the "= nil" so it
becomes simply `@State` private var selected: Int? to satisfy SwiftLint and rely
on Swift's implicit nil initialization for optionals.

In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift:
- Around line 414-613: Refactor updateMerchantInfo() by extracting
provider-specific normalization into two helper methods (e.g.,
normalizeCTXMerchant(merchantInfo:)->DashSpendNormalizedState and
normalizePiggyCardsState(giftCards:sourceId:)->DashSpendNormalizedState) so the
method only orchestrates fetches and applies a single normalized state on the
MainActor; move all DTO parsing and reconciliation logic currently using
ctxSpendRepository.getMerchant, PiggyCardsRepository.getGiftCards,
parseWholeDollarDenomination, providerConfiguredIsFixed, denomDiscounts,
denomInventory, rangeMin/rangeMax, etc. into those helpers (they should compute
isFixedDenomination, minimumAmount, maximumAmount, denominations,
savingsFraction, denominationDiscounts and denominationInventory), then call
await MainActor.run once to set self.* from the returned
DashSpendNormalizedState and finish with the existing
isLoading/objectWillChange/checkAmountForErrors flow.
🪄 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: 472b8ba9-3673-488b-b70e-6b60e366e57c

📥 Commits

Reviewing files that changed from the base of the PR and between d57dc1c and fb0abff.

📒 Files selected for processing (4)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Sources/Models/Transactions/SendCoinsService.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift

Comment thread DashWallet/Sources/Models/Transactions/SendCoinsService.swift Outdated
Comment thread DashWallet/Sources/Models/Transactions/SendCoinsService.swift Outdated
Comment thread DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift Outdated
@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (1)

421-628: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Refactor updateMerchantInfo() to pass configured SwiftLint complexity limits.

This method is currently too complex (reported as 26), which is likely to fail linting and make future logic changes riskier. Split provider-specific branches into dedicated helpers and keep this method as orchestration only.

As per coding guidelines, "**/*.swift: Use SwiftFormat and SwiftLint for Swift code formatting and linting as configured in .swiftformat and .swiftlint.yml files."

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift around lines 421 - 628, The
updateMerchantInfo() function is too complex; refactor it into a thin
orchestrator that delegates provider-specific work to new helper methods.
Extract the CTX branch into a helper like loadCTXMerchantInfo(merchantId:) that
calls ctxSpendRepository.getMerchant(...), computes
newSavings/newMin/newMax/newDenominations and updates MainActor state, and
extract the PiggyCards branch into
loadPiggyCardsMerchantInfo(sourceId:merchantId:) that performs giftCards
aggregation (allFixedDenominations, denomDiscounts, denomInventory,
rangeMin/max/discount), computes
finalFixed/finalMin/finalMax/finalDenominations/finalSavings and updates
MainActor state (denominationDiscounts, denominationInventory,
isFixedDenomination, minimumAmount, maximumAmount, denominations,
savingsFraction). Keep updateMerchantInfo() responsible only for validation
(merchantId, requiresAuth), setting isLoading, invoking the appropriate helper
with await, and final common UI updates/error handling.
🧹 Nitpick comments (1)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (1)

584-587: ⚡ Quick win

Add emoji prefixes to DSLogger entries for log filtering consistency.

These new logs should use the repository’s emoji markers (for example, 🔍/🎯) to stay searchable and consistent with existing debugging conventions.

As per coding guidelines, "Use emoji debug markers for easy log filtering: 🎯 🌐 💾 🎨 🔍 📍 🗺️ (e.g., DSLogger.log("🔍 Message:"))."

Also applies to: 615-615, 664-664

🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift around lines 584 - 587, The
DSLogger.log call that prints piggy card mode and range is missing the
repository's emoji debug marker convention; update the DSLogger.log
invocation(s) (see DSLogger.log in this function and the other similar calls
around the piggy card logic) to prefix the message with the appropriate emoji
marker (e.g., "🔍" or "🎯") while keeping the existing payload that references
finalFixed, finalMin, finalMax and finalDenominations.count so logs remain
searchable and unchanged in content.
🤖 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 `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/Components/DashSpendFlexibleContent.swift:
- Around line 58-64: multipleActionEnabled currently omits the
payment-in-progress flag, and the button’s inProgress is hardcoded to false;
update the predicate in the multipleActionEnabled computed property to also
require !viewModel.isProcessingPayment, and change any places where the button’s
inProgress is set to false (e.g., the multiple-mode purchase button usage around
the inProgress parameter) to use viewModel.isProcessingPayment so the UI
disables and shows progress while a payment is processing; apply the same change
to the similar logic referenced at the other occurrence (lines 223–225).

---

Outside diff comments:
In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift:
- Around line 421-628: The updateMerchantInfo() function is too complex;
refactor it into a thin orchestrator that delegates provider-specific work to
new helper methods. Extract the CTX branch into a helper like
loadCTXMerchantInfo(merchantId:) that calls ctxSpendRepository.getMerchant(...),
computes newSavings/newMin/newMax/newDenominations and updates MainActor state,
and extract the PiggyCards branch into
loadPiggyCardsMerchantInfo(sourceId:merchantId:) that performs giftCards
aggregation (allFixedDenominations, denomDiscounts, denomInventory,
rangeMin/max/discount), computes
finalFixed/finalMin/finalMax/finalDenominations/finalSavings and updates
MainActor state (denominationDiscounts, denominationInventory,
isFixedDenomination, minimumAmount, maximumAmount, denominations,
savingsFraction). Keep updateMerchantInfo() responsible only for validation
(merchantId, requiresAuth), setting isLoading, invoking the appropriate helper
with await, and final common UI updates/error handling.

---

Nitpick comments:
In `@DashWallet/Sources/UI/Explore`
Dash/Views/DashSpend/DashSpendPayViewModel.swift:
- Around line 584-587: The DSLogger.log call that prints piggy card mode and
range is missing the repository's emoji debug marker convention; update the
DSLogger.log invocation(s) (see DSLogger.log in this function and the other
similar calls around the piggy card logic) to prefix the message with the
appropriate emoji marker (e.g., "🔍" or "🎯") while keeping the existing payload
that references finalFixed, finalMin, finalMax and finalDenominations.count so
logs remain searchable and unchanged in content.
🪄 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: 07f05e82-4804-4066-8ed0-99c115311bdf

📥 Commits

Reviewing files that changed from the base of the PR and between fb0abff and 91a761a.

📒 Files selected for processing (6)
  • DashWallet/Sources/Models/Transactions/SendCoinsService.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFixedContent.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendFlexibleContent.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/Components/DashSpendSinglePanel.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift

@romchornyi
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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