Skip to content

fix: clear cached transactions when switching networks#741

Merged
bfoss765 merged 5 commits intofeature/piggycardsfrom
fix/PC-testnet-transactions
Dec 15, 2025
Merged

fix: clear cached transactions when switching networks#741
bfoss765 merged 5 commits intofeature/piggycardsfrom
fix/PC-testnet-transactions

Conversation

@bfoss765
Copy link
Copy Markdown
Contributor

@bfoss765 bfoss765 commented Dec 5, 2025

Summary

  • Fixed bug where testnet transactions appeared on mainnet view (and vice versa) until app restart
  • Added network change observer to HomeViewModel that clears all cached transaction data when switching networks
  • After clearing cache, fresh transactions are loaded from the new network's wallet

Root Cause

The HomeViewModel.shared singleton cached transaction data (txItems, txByHash, crowdNodeTxSet, coinJoinTxSets) but never cleared it when the network changed. Although DWCurrentNetworkDidChangeNotification was being posted during network switches, HomeViewModel didn't observe it.

Changes

  • Added observeNetworkChange() method to subscribe to DWCurrentNetworkDidChangeNotification
  • Added clearCachedData() method that:
    • Clears txItems (transaction groups displayed in UI)
    • Clears txByHash (transaction lookup map)
    • Resets crowdNodeTxSet
    • Clears coinJoinTxSets
    • Calls reloadTxsAndShortcuts() to fetch fresh data from the new network's wallet

Test plan

  • Switch from testnet to mainnet after making transactions on testnet
  • Verify testnet transactions do not appear on mainnet
  • Switch from mainnet to testnet
  • Verify mainnet transactions do not appear on testnet
  • Verify transactions reload correctly after each switch

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • PiggyCards integration for ordering and purchasing gift cards; expanded payment/order flows and gift card caching
    • Enhanced barcode handling with URL-extraction, image scanning and multi-format support
  • Improvements

    • Provider-aware UI: provider logos, per-denomination discounts, refined denomination/limit handling, loading and long-poll timeout states
    • Better percentage formatting and clearer discount displays
    • Explicit authentication gating before sending payments
    • Home cache invalidation on network change
  • Bug Fixes

    • Fixed markdown formatting in localized app description

✏️ Tip: You can customize this high-level summary in your review settings.

When switching between testnet and mainnet, the HomeViewModel singleton
was retaining cached transaction data from the previous network. This
caused testnet transactions to appear on mainnet (and vice versa) until
the app was restarted.

Added an observer for DWCurrentNetworkDidChangeNotification that clears
all cached transaction data (txItems, txByHash, crowdNodeTxSet,
coinJoinTxSets) and reloads fresh data from the new network's wallet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 5, 2025

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • DashWallet.xcodeproj/project.pbxproj

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

Walkthrough

Adds PiggyCards gift-card provider support across database, models, services, caching, token refresh (actor), ordering flow, barcode scanning, UI and metadata; includes a DB migration to add a nullable provider column and wires provider/sourceId through DAOs and view models.

Changes

Cohort / File(s) Change Summary
Config & Docs
\.gitignore, CLAUDE\.md, fastlane/metadata/ja/description\.txt, DashWallet.xcodeproj/.../dashwallet.xcscheme
Add SQLite temp ignores; add extensive PiggyCards documentation and guidance; fix Japanese metadata bullet formatting; change Xcode scheme buildConfiguration to Testflight.
DB Migration
DashWallet/Sources/Infrastructure/Database/DatabaseConnection.swift, DashWallet/Sources/Infrastructure/Database/Migrations/AddProviderToGiftCardsTable.swift
Add migration AddProviderToGiftCardsTable (version 20251120150000) and include it in migrations list to add a nullable provider column to gift_cards.
Core Models
DashWallet/Sources/Models/Explore Dash/Model/Entites/GiftCard.swift, .../ExplorePointOfUse.swift, .../PiggyCardsModels.swift
Add optional provider to GiftCard; add sourceId to GiftCardProviderInfo; introduce many PiggyCards API/order models and supporting types.
PiggyCards Service & Constants
.../DashSpend/PiggyCardsConstants.swift, .../PiggyCardsEndpoint.swift, .../PiggyCardsAPI.swift, .../PiggyCardsRepository.swift, .../PiggyCardsTokenService.swift, .../PiggyCardsCache.swift
New constants (staging URI, token expiry, fees, polling delay); new endpoints (brands, giftcards, createOrder, orderStatus, exchangeRate); generic request overload; actor-based token refresh; new order-based purchase flow (orderGiftCard); in-memory cache and selection logic.
DAO & DB Connection
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift, .../Model/DAO/GiftCardsDAO.swift, .../Infrastructure/Database Connection/ExploreDatabaseConnection.swift
MerchantDAO extracts and propagates sourceId; GiftCardsDAO persists provider; add test-only insertion of PiggyCards test merchant with FTS trigger handling (guarded by DEBUG/Testflight).
Barcode & Utilities
DashWallet/Sources/Models/Explore Dash/Services/BarcodeScanner.swift, DashWallet/Sources/Utils/PercentageFormatter.swift
New BarcodeScanner (URL param extraction, download, Vision-based scan) with BarcodeFormat/BarcodeResult; new PercentageFormatter for percent/basis-point formatting.
Send & CTX
DashWallet/Sources/Models/Transactions/SendCoinsService.swift, DashWallet/Sources/Models/Explore Dash/Services/DashSpend/CTX/CTXSpendRepository.swift
SendCoinsService now performs main-actor authenticate() (biometric/PIN) before signing; added CTX logging for gift-card fetch.
UI – Merchant Details & Providers
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/POIDetailsViewController.swift, .../POIDetailsView.swift, .../POIDetailsViewModel.swift
Attempt PiggyCards token refresh before pay flow; merchant struct receives giftCardProviders; provider ordering and discount formatting changed; selected provider determined from sorted list.
UI – DashSpend Flow
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift, .../DashSpendPayViewModel.swift, .../DashSpendConfirmationDialog.swift
Add loading branch to prevent flicker; provider-specific denominations and per-denomination discounts; provider-specific payment routing (CTX vs PiggyCards) and metadata tagging; centralized percentage formatting.
UI – Gift Card Details
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift, .../GiftCardDetailsViewModel.swift
Long-polling support (retries up to 40), claim-link detection/handling, provider-aware barcode/claim display; new UI state fields (isClaimLink, hasBeenPollingForLongTime, provider); provider-specific fetch flows.
UI – Merchant List & Cells
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift, .../MerchantItemCell.swift, .../AllMerchantLocationsDataProvider.swift
Removed debug prints; compute max discount across giftCardProviders when present; simplified discount presentation and ordering.
Metadata & Home
DashWallet/Sources/UI/Home/Tx Metadata/GiftCardMetadataProvider.swift, DashWallet/Sources/UI/Home/Views/HomeViewModel.swift, DashWallet/Sources/Models/Tx Metadata/ServiceName.swift
Add gift card DAO observer and batch updater updateMetadataForGiftCards; observe network-change to clear caches; add piggyCards case to ServiceName.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as POIDetailsView
    participant VM as DashSpendPayViewModel
    participant TokenSvc as PiggyCardsTokenService
    participant Repo as PiggyCardsRepository
    participant API as PiggyCardsAPI
    participant Cache as PiggyCardsCache
    participant Coins as SendCoinsService
    participant DB as GiftCardsDAO

    User->>UI: Tap "Buy Gift Card"
    UI->>VM: purchaseGiftCardAndPay()
    VM->>TokenSvc: refreshTokenIfNeeded()
    alt token refresh required
        TokenSvc->>API: performAutoLogin()
        API-->>TokenSvc: accessToken
    end
    VM->>Repo: orderGiftCard(merchantId, amount)
    Repo->>Cache: getGiftCards(forMerchant)
    alt cache miss
        Repo->>API: getGiftCards(...)
        API-->>Repo: [PiggyCardsGiftcard]
        Repo->>Cache: storeGiftCards(...)
    end
    Repo->>Repo: selectGiftCard(...)
    Repo->>API: createOrder(PiggyCardsOrderRequest)
    API-->>Repo: PiggyCardsOrderResponse (payTo, orderId)
    Repo-->>VM: GiftCardInfo
    VM->>Coins: sendCoins(address, amount)
    Coins->>VM: authenticate() (main actor)
    VM-->>Coins: signed tx
    Coins->>DB: saveGiftCard(provider: "piggycards")
    DB-->>Coins: saved
    Coins-->>VM: tx confirmed
    VM->>UI: show success and gift card details
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas needing careful review:
    • PiggyCardsRepository.swift — complex orchestration, selection, order processing, currency/fee calculations.
    • PiggyCardsTokenService.swift — actor concurrency and refresh correctness.
    • DashSpendPayViewModel.swift — provider switch, per-denomination discounts, payment routing and metadata.
    • GiftCardDetailsViewModel.swift — long-polling, retry/backoff and state transitions.
    • SendCoinsService.swift — main-actor authenticate() integration and signing gating.
    • Database migration, DAOs and test-only DB seeding — schema changes, null handling, FTS trigger management.
    • BarcodeScanner.swift — async image handling and Vision usage.

Possibly related PRs

Suggested reviewers

  • HashEngineering

Poem

🐰 I hop through migrations, tokens, and queues,
Scanning barcodes and fetching new views.
With PiggyCards tucked in a tiny pouch,
Orders, caches, and metadata—what a slouch! ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title does not match the changeset. While the PR objectives indicate the primary fix is clearing cached transactions on network switch, the actual changes are substantially broader, including significant PiggyCards integration work, barcode scanning, gift card handling, and extensive database/API changes. Update the title to accurately reflect the full scope of changes, such as 'feat: add PiggyCards integration with barcode scanning, gift card management, and network cache clearing' or split into multiple PRs.
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 17

Caution

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

⚠️ Outside diff range comments (2)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (2)

182-191: SQL injection vulnerability: Use parameterized queries.

The merchantId is interpolated directly into the SQL string. While it likely comes from a trusted source (the database), parameterized queries are safer and more consistent. Line 639 correctly uses parameterized queries with db.prepare(providersQuery, merchant.merchantId).

 #if PIGGYCARDS_ENABLED
 let providersQuery = """
-    SELECT provider, savingsPercentage, denominationsType, sourceId FROM gift_card_providers
-    WHERE merchantId = '\(merchant.merchantId)'
+    SELECT provider, savingsPercentage, denominationsType, sourceId FROM gift_card_providers
+    WHERE merchantId = ?
 """
 #else
 let providersQuery = """
     SELECT provider, savingsPercentage, denominationsType FROM gift_card_providers
-    WHERE merchantId = '\(merchant.merchantId)' AND provider = 'CTX'
+    WHERE merchantId = ? AND provider = 'CTX'
 """
 #endif
 // Then use: try db.prepare(providersQuery, merchant.merchantId)

423-433: Same SQL injection issue in second query location.

This is the same issue as lines 182-191 - string interpolation instead of parameterized query.

🧹 Nitpick comments (39)
DashWallet/Sources/Models/Transactions/SendCoinsService.swift (1)

66-75: Address SwiftLint warnings.

Two linting issues flagged:

  1. @MainActor attribute should be on its own line
  2. usedBiometrics closure parameter is unused
-        @MainActor func authenticate() async -> Bool {
+        @MainActor
+        func authenticate() async -> Bool {
             return await withCheckedContinuation { continuation in
                 DSAuthenticationManager.sharedInstance().authenticate(
                     withPrompt: nil,
                     usingBiometricAuthentication: DWGlobalOptions.sharedInstance().biometricAuthEnabled,
                     alertIfLockout: true
-                ) { authenticatedOrSuccess, usedBiometrics, cancelled in
+                ) { authenticatedOrSuccess, _, cancelled in
                     continuation.resume(returning: authenticatedOrSuccess && !cancelled)
                 }
             }
         }
DashWallet/Sources/UI/Home/Views/HomeViewModel.swift (1)

102-118: LGTM with architectural note.

The integration of observeNetworkChange() into the initialization flow is well-placed and consistent with the existing observer pattern.

However, per coding guidelines, ViewModels should use @MainActor class decorator. This class currently uses a background queue (self.queue) for heavy operations and manually dispatches to main thread, which may be why @MainActor was omitted. Consider whether the threading model should be refactored to align with the guideline, or document why this ViewModel requires a different pattern.

Based on coding guidelines:

**/*ViewModel.swift: Use @mainactor class decorator for ViewModels to ensure UI updates on main thread

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/POIDetailsViewController.swift (2)

195-213: PiggyCards token refresh before pay screen looks sound.

The Task‑based flow with a PiggyCards‑only token refresh, await MainActor.run for navigation, and early return on failure is coherent and avoids pushing the pay screen when the session is invalid. Optional: if you anticipate multiple callers needing this pattern, consider centralizing the “refresh‑then‑show‑pay‑screen” logic in a shared helper to keep behavior consistent across entry points.


319-327: CTX and PiggyCards token refresh handling is correct; consider deduplicating dialog text.

  • tryRefreshCtxToken() and tryRefreshPiggyCardsToken() both surface a consistent “session expired” flow and correctly logout on tokenRefreshFailed/unauthorized, while treating other errors as transient and preserving the session.
  • UI work (logout, dialogs) is dispatched to the main actor where needed, and non‑fatal errors are logged via DSLogger.

You might want to DRY up the repeated session‑expired dialog (heading + body + button text) into a small helper to keep capitalization/copy synchronized between CTX and PiggyCards paths.

Also applies to: 329-356

DashWallet/Sources/Models/Explore Dash/Model/DAO/GiftCardsDAO.swift (1)

58-71: Provider column wiring is correct; align multi‑line parameters to satisfy SwiftLint.

  • Insertion now persists both note and provider, and the two update helpers reconstruct GiftCard instances while preserving provider (and note where appropriate). This keeps the in‑memory cache consistent with the DB and matches the new provider column added in migrations.
  • SwiftLint warns about vertical_parameter_alignment_on_call for the multi‑line insert/update calls; you can tweak the indentation to silence it without behavior change, e.g.:
-            let insert = GiftCard.table.insert(or: .replace,
-                                              GiftCard.txId <- dto.txId,
-                                              GiftCard.merchantName <- dto.merchantName,
-                                              GiftCard.merchantUrl <- dto.merchantUrl,
-                                              GiftCard.price <- dto.price.description,
-                                              GiftCard.number <- dto.number,
-                                              GiftCard.pin <- dto.pin,
-                                              GiftCard.barcodeValue <- dto.barcodeValue,
-                                              GiftCard.barcodeFormat <- dto.barcodeFormat,
-                                              GiftCard.note <- dto.note,
-                                              GiftCard.provider <- dto.provider)
+            let insert = GiftCard.table.insert(
+                or: .replace,
+                GiftCard.txId <- dto.txId,
+                GiftCard.merchantName <- dto.merchantName,
+                GiftCard.merchantUrl <- dto.merchantUrl,
+                GiftCard.price <- dto.price.description,
+                GiftCard.number <- dto.number,
+                GiftCard.pin <- dto.pin,
+                GiftCard.barcodeValue <- dto.barcodeValue,
+                GiftCard.barcodeFormat <- dto.barcodeFormat,
+                GiftCard.note <- dto.note,
+                GiftCard.provider <- dto.provider
+            )

(and similarly for the two update calls).

Also applies to: 115-126, 147-157

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayScreen.swift (1)

291-291: Remove empty line after opening brace.

Per SwiftLint's vertical_whitespace_opening_braces rule.

 private func purchaseGiftCard() {
-
     Task {
DashWallet/Sources/Infrastructure/Database/Migrations/AddProviderToGiftCardsTable.swift (1)

28-28: Add trailing newline.

Per SwiftLint's trailing_newline rule, files should end with a single newline character.

DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsTokenService.swift (2)

94-123: Avoid force unwrap on refreshTask at line 122.

While the force unwrap is likely safe within this actor context (since refreshTask is set immediately before at line 103), this triggers SwiftLint and could be avoided with optional binding for defensive coding.

Apply this diff:

-        _ = try await refreshTask!.value
+        guard let task = refreshTask else { return }
+        _ = try await task.value

Alternatively, you could capture the task directly:

         // Start new refresh task
-        refreshTask = Task {
+        let task = Task {
             defer {
                 refreshTask = nil
                 isRefreshing = false
             }
 
             isRefreshing = true
             let success = try await performRefresh()
 
             if success {
                 DSLogger.log("PiggyCards: Token refresh completed successfully")
             } else {
                 DSLogger.log("PiggyCards: Token refresh failed")
                 throw DashSpendError.tokenRefreshFailed
             }
 
             return success
         }
+        refreshTask = task
 
-        _ = try await refreshTask!.value
+        _ = try await task.value

91-92: Note: isRefreshing flag appears unused.

The isRefreshing property is set in the actor but never read. If it's intended for future use or debugging, consider adding a comment; otherwise, it can be removed to reduce dead code.

DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift (2)

156-163: Use isEmpty check instead of comparing count to zero.

Per SwiftLint, prefer checking existence rather than counting rows.

-            let checkQuery = "SELECT COUNT(*) FROM merchant WHERE merchantId = '\(testMerchantId)'"
-            let count = try db.scalar(checkQuery) as? Int64 ?? 0
-
-            if count > 0 {
+            let checkQuery = "SELECT 1 FROM merchant WHERE merchantId = '\(testMerchantId)' LIMIT 1"
+            let exists = try db.scalar(checkQuery) != nil
+
+            if exists {

252-256: Remove vertical whitespace before closing brace.

SwiftLint flagged an empty line before the closing brace at line 253.

             print("✅ PiggyCards test merchant added successfully")
-
         } catch {
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsAPI.swift (2)

79-82: Consider using guard let instead of explicit nil comparison.

This is more idiomatic Swift.

         default:
-            let token = PiggyCardsTokenService.shared.accessToken
-            if token == nil {
+            guard PiggyCardsTokenService.shared.accessToken != nil else {
                 throw DashSpendError.unauthorized
             }
         }

45-47: Generic catch-and-rethrow is redundant.

The catch { throw error } block simply rethrows any unhandled error, which is the default behavior. It can be removed unless there's a future plan to add error transformation here.

         } catch HTTPClientError.statusCode(let r) where r.statusCode == 400 {
             if target.path.contains("/verify-otp") {
                 throw DashSpendError.invalidCode
             }
             throw HTTPClientError.statusCode(r)
-        } catch {
-            throw error
         }
DashWallet/Sources/Utils/PercentageFormatter.swift (2)

33-42: Simplify the decimal detection logic.

The hasDecimal variable at lines 34-35 includes a check that's repeated in line 38, making the logic harder to follow. The condition (percent * 10).truncatingRemainder(dividingBy: 1) != 0 doesn't add clarity since it would only differ from the simpler check for values with sub-decimal precision that Double can't represent accurately anyway.

-        // Check if the value has a meaningful decimal component
-        let hasDecimal = (percent * 10).truncatingRemainder(dividingBy: 1) != 0 ||
-                         percent.truncatingRemainder(dividingBy: 1) != 0
-
-        // Use 1 decimal place if there's a fractional part, otherwise use whole numbers
-        if hasDecimal && percent.truncatingRemainder(dividingBy: 1) != 0 {
+        // Use 1 decimal place if there's a fractional part, otherwise use whole numbers
+        if percent.truncatingRemainder(dividingBy: 1) != 0 {
             return String(format: "\(sign)%.1f\(percentSymbol)", percent)
         } else {
             return String(format: "\(sign)%.0f\(percentSymbol)", percent)
         }

21-22: Remove empty line after opening brace.

Per SwiftLint, avoid vertical whitespace after opening braces.

 enum PercentageFormatter {
-
     /// Format a percentage value with appropriate decimal places
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsCache.swift (4)

34-36: Consider making the disabled gift cards list configurable.

The hardcoded disabledGiftCards dictionary for brandId 174 may need updates over time. Consider loading this from a configuration file or remote config to avoid code changes when the disabled list changes.


62-78: Exchange rate cache lacks expiration mechanism.

Exchange rates are volatile and cached indefinitely. While the relevant code snippet from PiggyCardsRepository.swift shows cache-first retrieval, there's no TTL to prevent stale rates from being used for extended periods.

Consider adding timestamp tracking and a TTL check:

 // Cache for exchange rates
-private var exchangeRateCache: [String: PiggyCardsExchangeRateResult] = [:]
+private var exchangeRateCache: [String: (rate: PiggyCardsExchangeRateResult, timestamp: Date)] = [:]
 private let exchangeRateQueue = DispatchQueue(label: "piggyCardsCache.exchangeRate", attributes: .concurrent)
+private let exchangeRateTTL: TimeInterval = 300 // 5 minutes

84-126: Gift card selection logic is well-structured but has repeated string processing.

The priority-based selection is clear, but priceType.trimmingCharacters(in: .whitespaces).lowercased() is repeated 4 times. Consider pre-processing once per card for efficiency.

 func selectGiftCard(from cards: [PiggyCardsGiftcard], forAmount amount: Double) -> PiggyCardsGiftcard? {
     // Filter out disabled cards
     let enabledCards = cards.filter { card in
         guard let disabledNames = disabledGiftCards[card.brandId] else { return true }
         return !disabledNames.contains { card.name.contains($0) }
     }.filter { $0.quantity > 0 } // Only available cards
+    
+    // Pre-compute normalized price types for efficiency
+    let cardsWithNormalizedType = enabledCards.map { card in
+        (card: card, normalizedPriceType: card.priceType.trimmingCharacters(in: .whitespaces).lowercased())
+    }

     // Priority 1: Instant delivery fixed cards with exact denomination
-    if let instantCard = enabledCards.first(where: { card in
-        card.priceType.trimmingCharacters(in: .whitespaces).lowercased() == PiggyCardsPriceType.fixed.rawValue &&
+    if let instantCard = cardsWithNormalizedType.first(where: { item in
+        item.normalizedPriceType == PiggyCardsPriceType.fixed.rawValue &&
         // ... rest of conditions

133-134: Add trailing newline.

Static analysis indicates a missing trailing newline at end of file.

     func clearAllCaches() {
         clearGiftCardCache()
         clearExchangeRateCache()
     }
 }
+
DashWallet/Sources/Models/Explore Dash/Services/BarcodeScanner.swift (1)

120-122: Consider using os_log for error logging.

Per coding guidelines, production-safe logging should use os_log framework instead of silently swallowing errors. Adding debug logging would help diagnose barcode scanning failures.

+import os.log
+
+private let logger = Logger(subsystem: "org.dash.wallet", category: "BarcodeScanner")
+
 // In downloadAndScan:
         } catch {
+            #if DEBUG
+            logger.debug("🎨 Failed to download barcode image: \(error.localizedDescription)")
+            #endif
             return nil
         }
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (2)

199-202: Remove unused rowCount variable.

The rowCount variable is declared and incremented but never used. This appears to be debug code that was left behind.

-var rowCount = 0
 
 for row in rows {
-    rowCount += 1
     if let providerId = row[0] as? String,

Also applies to: 442-445, 641-644


658-676: Extra fallback logic for sourceId is defensive but verbose.

This third instance of sourceId handling includes additional fallback with String(describing:) and null string checks. While more robust, the inconsistency with other locations suggests this should be unified in a helper function.

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsView.swift (4)

153-163: Fix trailing closure syntax as flagged by SwiftLint.

Button with action closure followed by trailing closure for content should use explicit label: parameter.

-                        Button(action: {
-                            if let url = URL(string: claimLink) {
-                                UIApplication.shared.open(url)
-                            }
-                        }) {
+                        Button(action: {
+                            if let url = URL(string: claimLink) {
+                                UIApplication.shared.open(url)
+                            }
+                        }, label: {
                             Text(NSLocalizedString("View your gift card details", comment: "DashSpend"))
                                 .font(.subheadline)
                                 .fontWeight(.medium)
                                 .foregroundColor(.dashBlue)
                                 .frame(maxWidth: .infinity, alignment: .center)
-                        }
+                        })

202-211: Fix trailing closure syntax for copy button.

Same SwiftLint warning applies here. Use explicit label: parameter.

-                                        Button(action: {
-                                            UIPasteboard.general.string = cardNumber
-                                            // TODO: Show toast
-                                        }) {
+                                        Button(action: {
+                                            UIPasteboard.general.string = cardNumber
+                                            // TODO: Show toast
+                                        }, label: {
                                             Image("icon_copy_outline")
                                                 .resizable()
                                                 .scaledToFit()
                                                 .tint(.primaryText)
                                                 .frame(width: 14, height: 14)
-                                        }
+                                        })

232-241: Fix trailing closure syntax for PIN copy button.

Same SwiftLint warning applies here.

-                                        Button(action: {
-                                            UIPasteboard.general.string = cardPin
-                                            // TODO: Show toast
-                                        }) {
+                                        Button(action: {
+                                            UIPasteboard.general.string = cardPin
+                                            // TODO: Show toast
+                                        }, label: {
                                             Image("icon_copy_outline")
                                                 .resizable()
                                                 .scaledToFit()
                                                 .tint(.primaryText)
                                                 .frame(width: 14, height: 14)
-                                        }
+                                        })

399-403: DateFormatter created on every access is inefficient.

This computed property creates a new DateFormatter instance every time it's accessed. Consider making it a static constant or lazy property since the format is fixed.

-    private var dateFormatter: DateFormatter {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "MMMM dd, yyyy 'at' h:mm a"
-        return formatter
-    }
+    private static let dateFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "MMMM dd, yyyy 'at' h:mm a"
+        return formatter
+    }()

Then update line 76 to use Self.dateFormatter.

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift (4)

34-36: Remove redundant optional initialization.

SwiftLint correctly flags that initializing an optional with nil is redundant since that's the default.

     var isClaimLink: Bool = false
     var hasBeenPollingForLongTime: Bool = false
-    var provider: String? = nil
+    var provider: String?
 }

185-190: Replace unused optional binding with nil check.

SwiftLint flags this pattern. Using != nil is clearer and more idiomatic.

     private func fetchGiftCardInfo() async {
         guard let giftCard = await giftCardsDAO.get(byTxId: txId),
-              let _ = giftCard.note else {
+              giftCard.note != nil else {
             stopTicker()
             return
         }

213-219: Same unused binding pattern in CTX fetch.

Apply the same fix here.

     private func fetchCTXGiftCardInfo() async {
         guard let giftCard = await giftCardsDAO.get(byTxId: txId),
-              let _ = giftCard.note,
+              giftCard.note != nil,
               ctxSpendRepository.isUserSignedIn else {
             stopTicker()
             return
         }

274-364: Consider extracting helper methods to reduce cyclomatic complexity.

SwiftLint flags complexity of 11 (limit is 10). The nested conditionals for claimCode vs claimLink handling could be extracted into separate helper methods.

For example, extract the claim code processing:

private func processClaimCode(_ claimCode: String, pin: String?, barcodeLink: String?) async {
    await giftCardsDAO.updateCardDetails(txId: txId, number: claimCode, pin: pin)
    
    if let barcodeLink = barcodeLink, !barcodeLink.isEmpty,
       let result = await BarcodeScanner.downloadAndScan(from: barcodeLink) {
        let cleanValue = result.value.replacingOccurrences(of: " ", with: "")
            .replacingOccurrences(of: "-", with: "")
        await giftCardsDAO.updateBarcode(txId: txId, value: cleanValue, format: result.format.rawValue)
    } else {
        let cleanCode = claimCode.replacingOccurrences(of: " ", with: "")
            .replacingOccurrences(of: "-", with: "")
        await giftCardsDAO.updateBarcode(txId: txId, value: cleanCode, format: "CODE128")
    }
}
DashWallet/Sources/Models/Explore Dash/Model/Entites/PiggyCardsModels.swift (2)

129-133: Remove redundant string enum value.

SwiftLint correctly flags that case userId = "userId" is redundant since the raw value matches the case name.

 struct PiggyCardsSignupResponse: Codable {
     let userId: String?

     enum CodingKeys: String, CodingKey {
-        case userId = "userId"
+        case userId
     }
 }

325-329: Remove redundant string enum values.

All three cases have raw values matching their case names.

 enum PiggyCardsPriceType: String {
-    case fixed = "fixed"
-    case range = "range"
-    case option = "option"
+    case fixed
+    case range
+    case option
 }
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (2)

165-166: Remove empty else block.

This empty else block serves no purpose and should be removed.

             }
-        } else {
         }

241-272: PiggyCards payment flow uses hardcoded satoshi conversion.

The conversion giftCardInfo.amount * 100_000_000 assumes amount is in DASH. This is correct but consider using a named constant for clarity and to prevent magic number issues.

+            let satoshisPerDash: Double = 100_000_000
             // Convert DASH amount to satoshis (1 DASH = 100,000,000 satoshis)
-            let dashAmountInSatoshis = UInt64(giftCardInfo.amount * 100_000_000)
+            let dashAmountInSatoshis = UInt64(giftCardInfo.amount * satoshisPerDash)
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift (3)

246-259: Remove empty debug logging blocks.

These empty if blocks for JSON logging serve no purpose in the current state.

-        // 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))
             return try await processOrderResponse(orderResponse, selectedCard: selectedCard, giftCards: giftCards, fiatCurrency: fiatCurrency)
         } 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")

353-356: Another empty for loop to remove.

Same debug artifact pattern.

             guard let cards = response.data else {
                 throw DashSpendError.merchantUnavailable
             }

-            // Log details of each card
-            for (index, card) in cards.enumerated() {
-            }

             // Cache the cards for later use in order creation
             PiggyCardsCache.shared.storeGiftCards(cards, forMerchant: merchantId)

268-271: Fix vertical parameter alignment as flagged by SwiftLint.

-    private func processOrderResponse(_ orderResponse: PiggyCardsOrderResponse,
-                                     selectedCard: PiggyCardsGiftcard,
-                                     giftCards: [PiggyCardsGiftcard],
-                                     fiatCurrency: String) async throws -> GiftCardInfo {
+    private func processOrderResponse(
+        _ orderResponse: PiggyCardsOrderResponse,
+        selectedCard: PiggyCardsGiftcard,
+        giftCards: [PiggyCardsGiftcard],
+        fiatCurrency: String
+    ) async throws -> GiftCardInfo {
CLAUDE.md (2)

1571-1571: Fix markdown linting violation: bare URL should use link syntax.

Line 1571 contains a bare URL which violates markdown linting rules (MD034).

- Based on real issues encountered and resolved:
+ Based on [real issues encountered and resolved](relevant_link):

Or if not linking to external resource, simply ensure URLs are wrapped in markdown link syntax where appropriate.


1589-1598: Add language specifier to fenced code block.

The code block starting at line 1589 is missing the language identifier. Add bash to the opening fence for proper syntax highlighting:

-```
+```bash
# Find the most recent explore.db for your running simulator
find ~/Library/Developer/CoreSimulator/Devices/*/data/Containers/Data/Application/*/Documents/explore.db -type f -exec stat -f '%Sm %N' -t '%Y-%m-%d %H:%M:%S' {} \; | sort -r | head -1
-```
+```

Comment thread DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift Outdated
Comment thread DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
Comment thread DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
bfoss765 and others added 2 commits December 5, 2025 17:30
- MerchantDAO: Extract duplicated sourceId coercion logic into
  extractSourceId() helper method that handles String, Int64, Int
  types and nil/empty values

- ExploreDatabaseConnection: Wrap FTS trigger drop/create operations
  in a transaction for atomicity and rollback on error

- GiftCardDetailsViewModel: Lower longPollingThreshold from 40 to 27
  so "code is being generated" message shows after ~40s instead of
  only on the final retry

- HomeViewModel: Fix thread safety in clearCachedData() by dispatching
  cache clearing to self.queue where those properties are accessed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added explicit documentation that permission to commit/push applies
only to the specific changes discussed, not to subsequent changes.
Includes example scenario and common mistake to avoid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
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/GiftCardDetailsViewModel.swift (1)

129-157: Ensure isClaimLink is reset when number stops being a URL

In loadGiftCard, isClaimLink is only ever set to true:

if let number = card.number, number.starts(with: "http") {
    self.uiState.isClaimLink = true
}

If a PiggyCards order were to move from a link (claimLink) to a plain code (claimCode) on a later refresh, this flag would stay true and the UI could render the wrong state. Consider explicitly resetting it in the non‑URL case:

-            if let number = card.number, number.starts(with: "http") {
-                self.uiState.isClaimLink = true
-            }
+            if let number = card.number, number.starts(with: "http") {
+                self.uiState.isClaimLink = true
+            } else {
+                self.uiState.isClaimLink = false
+            }

This keeps the state derived purely from the current card.number.

♻️ Duplicate comments (2)
DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift (1)

188-217: Fix inconsistent denominationsType casing and savingsPercentage values.

Two issues based on past learnings remain unaddressed:

  1. Casing mismatch: Line 190 uses 'Fixed' (uppercase) but line 213 uses 'fixed' (lowercase). Downstream filtering expects lowercase values.

  2. savingsPercentage mismatch: The merchant table has 1000 (basis points = 10%) but gift_card_providers has 10 (which would be 0.1%). Both tables must use consistent basis point values.

Apply this diff:

                         1000,
-                        'Fixed',
+                        'fixed',
                         'online',

And for the gift_card_providers insert:

                         'PiggyCards',
                         '177',
-                        10,
+                        1000,
                         'fixed',

Based on learnings, denominationsType values must use lowercase and savingsPercentage values must be consistent across both tables in basis points.

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift (1)

240-245: Unify barcode format string ("CODE128" vs "CODE_128") and centralize it

Line 243 uses "CODE128":

format: "CODE128"

while the PiggyCards fallbacks at lines 318 and 328 use "CODE_128":

format: "CODE_128"

This inconsistency can cause downstream issues if any code relies on the exact format string (e.g., when interpreting barcodeFormat from the DB). It’s also been flagged in a previous review.

To keep things consistent, consider:

  • Standardizing on a single canonical string (e.g. "CODE128" to match the CTX path), and
  • Extracting it to a shared constant or enum raw value.

For example:

private enum BarcodeFormatString {
    static let code128 = "CODE128"
}

and then:

-    format: "CODE128"
+    format: BarcodeFormatString.code128
-    format: "CODE_128"
+    format: BarcodeFormatString.code128

This avoids subtle mismatches across providers.

Also applies to: 313-329

🧹 Nitpick comments (8)
DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift (2)

156-163: Consider using EXISTS check instead of COUNT(*) for efficiency.

Using COUNT(*) retrieves the total count when you only need to know if at least one row exists. An EXISTS or LIMIT 1 pattern is more efficient.

Apply this diff:

-            let checkQuery = "SELECT COUNT(*) FROM merchant WHERE merchantId = '\(testMerchantId)'"
-            let count = try db.scalar(checkQuery) as? Int64 ?? 0
+            let checkQuery = "SELECT 1 FROM merchant WHERE merchantId = '\(testMerchantId)' LIMIT 1"
+            let exists = try db.scalar(checkQuery) != nil

-            if count > 0 {
+            if exists {
                 print("🎯 PiggyCards test merchant already exists, skipping")
                 return
             }

254-259: Minor: Remove empty line before closing brace.

SwiftLint flags vertical whitespace before closing braces.

Apply this diff:

             }

             print("✅ PiggyCards test merchant added successfully")
-
         } catch {
             print("🎯 Error adding PiggyCards test merchant: \(error)")
         }
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (2)

207-216: Consider using parameterized queries consistently for merchantId.

The allLocations method (lines 633-644) correctly uses parameterized queries with ? placeholders, but these provider queries still use string interpolation. While merchantId originates from the database (low injection risk), using parameterized queries consistently is a defensive best practice.

Example for the first occurrence (apply similarly to line 441):

 #if PIGGYCARDS_ENABLED
 let providersQuery = """
     SELECT provider, savingsPercentage, denominationsType, sourceId FROM gift_card_providers
-    WHERE merchantId = '\(merchant.merchantId)'
+    WHERE merchantId = ?
 """
+let statement = try db.prepare(providersQuery, merchant.merchantId)
 #else
 let providersQuery = """
     SELECT provider, savingsPercentage, denominationsType FROM gift_card_providers
-    WHERE merchantId = '\(merchant.merchantId)' AND provider = 'CTX'
+    WHERE merchantId = ? AND provider = 'CTX'
 """
+let statement = try db.prepare(providersQuery, merchant.merchantId)
 #endif

Also applies to: 439-448


224-227: Remove unused rowCount variable.

The rowCount variable is declared and incremented but never used. If it's not needed for debugging, remove it to avoid dead code.

Apply this diff at each location:

-var rowCount = 0
-
 for row in rows {
-    rowCount += 1
     if let providerId = row[0] as? String,

Also applies to: 457-460, 646-649

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift (4)

34-36: UI state additions make sense; minor optional-init style nit

isClaimLink, hasBeenPollingForLongTime, and provider cleanly capture the new UI requirements. SwiftLint flags var provider: String? = nil as redundant; you can drop the = nil and rely on the default optional initialization.


49-51: Align retryCount semantics with polling duration comments

Right now retryCount is only incremented in the catch blocks of fetchCTXGiftCardInfo and fetchPiggyCardsGiftCardInfo, so:

  • hasBeenPollingForLongTime only flips to true after many errors, not after ~40 seconds of polling as the comments suggest.
  • maxRetries effectively caps consecutive error retries, not overall polling duration (successful-but-non‑fulfilled responses can continue indefinitely).

Given the comments tying maxRetries and longPollingThreshold directly to 1.5s polling intervals, it may be clearer to:

  • Increment retryCount once per polling attempt in fetchGiftCardInfo, and
  • Remove the extra increments from the catch blocks, using retryCount as a general “attempt count”.

Example adjustment:

     private func fetchGiftCardInfo() async {
-        guard let giftCard = await giftCardsDAO.get(byTxId: txId),
+        retryCount += 1
+
+        guard let giftCard = await giftCardsDAO.get(byTxId: txId),
               let _ = giftCard.note else {
             stopTicker()
             return
         }

-        // Check if we've been polling for more than 60 seconds
+        // Check if we've been polling for more than 60 seconds
         if retryCount >= longPollingThreshold {
             await MainActor.run {
                 self.uiState.hasBeenPollingForLongTime = true
             }
         }
     } catch {
-        retryCount += 1
         if retryCount >= maxRetries {
             await MainActor.run {
                 self.uiState.loadingError = error
             }
             stopTicker()
         }

(and same pattern in fetchPiggyCardsGiftCardInfo).

stopTicker() already resets retryCount and hasBeenPollingForLongTime, which is good and works with this model. Please double‑check the intended behavior and adjust if retryCount is meant to be “attempts” rather than “error retries”.

Also applies to: 192-211, 262-269, 355-363


185-191: Replace let _ = giftCard.note with an explicit non-nil check

SwiftLint flags the guard bindings using let _ = giftCard.note as unused optional bindings. For clarity and to satisfy unused_optional_binding, you can change both guards to:

-    guard let giftCard = await giftCardsDAO.get(byTxId: txId),
-          let _ = giftCard.note else {
+    guard let giftCard = await giftCardsDAO.get(byTxId: txId),
+          giftCard.note != nil else {
         stopTicker()
         return
     }

and similarly in fetchCTXGiftCardInfo.

Also applies to: 213-219


274-364: PiggyCards polling logic looks correct; consider extracting helpers to reduce complexity

The PiggyCards flow correctly:

  • Guards on orderId and sign‑in status.
  • Handles "complete"/"completed" by updating card details and barcode (with barcodeLink scan + claimCode fallbacks).
  • Handles link-based redemption via claimLink.
  • Maps "failed" / "rejected" / "cancelled" to a user‑friendly error.
  • Stops polling or keeps polling appropriately.

SwiftLint is flagging cyclomatic complexity (11), and there’s some repeated cleanup/normalization logic (e.g., the “cleanCode” fallbacks and the error‑timeout block). You could modestly simplify by extracting small helpers, for example:

  • private func updateBarcodeFromClaimCode(_ claimCode: String) async
  • private func handlePiggyCardsTerminalError() async

and reuse those for the two fallback branches and the error path. That should drop the complexity without changing behavior and also clean up the vertical whitespace up around if let firstCard = cards.first { that SwiftLint warns about.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42382e5 and a5fbe50.

📒 Files selected for processing (5)
  • CLAUDE.md (4 hunks)
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (8 hunks)
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift (5 hunks)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift (4 hunks)
  • DashWallet/Sources/UI/Home/Views/HomeViewModel.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{swift,swift.gyb}

📄 CodeRabbit inference engine (CLAUDE.md)

Use SwiftFormat/SwiftLint for Swift code formatting and linting (configurations in .swiftformat and .swiftlint.yml)

Files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
**/*.{h,m,mm,swift}

📄 CodeRabbit inference engine (CLAUDE.md)

Use 4-space indentation and maintain 180-character line limit (100 recommended)

Files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
**/*.swift

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.swift: Avoid @objcMembers wholesale exposure (enforced by custom SwiftLint rule)
Always use async/await for promises in Swift code, not completion handlers
Never force unwrap latitude/longitude coordinates; always use guard statements
Use guard statements and avoid force unwrapping optional properties like lastBounds
Add explicit return type annotations to compactMap closures when compiler inference fails
Use computed properties instead of inline #if conditional compilation statements in SwiftUI ViewBuilder
Use closure-based initialization for dictionaries with conditional compilation instead of inline conditionals
Wrap debug print statements in #if DEBUG blocks to prevent performance issues in release builds
Remove empty switch cases when cleaning up debug code; add break statement or remove case entirely
Use @StateObject private var viewModel in SwiftUI views for proper lifecycle management
Use NavigationStack for new SwiftUI navigation; use UIHostingController for legacy UIKit integration
Use Combine @published properties for reactive data flow between services and SwiftUI views
iOS Swift files should declare conformance using 'extension ClassName: ProtocolName' syntax, not inline
Use os_log framework for production-safe logging instead of print() statements
Implement fallback chains for optional API response fields with explicit unwrapping
Always validate CLLocationCoordinate2D.isValid before using coordinates
Use emoji debug markers in logs for easy filtering: 🎯 for goals, 🌐 for network, 💾 for database, 🎨 for UI

Files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
DashWallet/Sources/UI/**/*.swift

📄 CodeRabbit inference engine (CLAUDE.md)

All new UI components MUST be built with SwiftUI, not UIKit ViewControllers or Storyboards

Files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
**/*ViewModel.swift

📄 CodeRabbit inference engine (CLAUDE.md)

**/*ViewModel.swift: Use @mainactor class decorator for ViewModels to ensure UI updates on main thread
Implement @published properties in ViewModels for reactive data binding with SwiftUI

Files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
DashWallet/Sources/Models/**/*DAO.swift

📄 CodeRabbit inference engine (CLAUDE.md)

DashWallet/Sources/Models/**/*DAO.swift: Apply both rectangular bounds filtering (SQL optimization) AND circular distance filtering (accuracy) for location-based queries
Expand rectangular bounds by 50% when used for circular radius searches to ensure consistency
Filter merchants by provider when fetching details to avoid duplicate/incorrect data from multi-provider database
Always query git_card_providers table (not merchant table) for denomination type and provider-specific data

Files:

  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
🧠 Learnings (23)
📓 Common learnings
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to **/CTX*.swift : Use blockchain transaction ID (txid), not gift card UUID, when fetching gift cards from CTX API
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/Models/**/*DAO.swift : Always query git_card_providers table (not merchant table) for denomination type and provider-specific data
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 737
File: DashWallet/Sources/Models/Explore Dash/Model/Entites/PiggyCardsModels.swift:200-235
Timestamp: 2025-11-25T20:39:25.847Z
Learning: In DashWallet iOS gift card implementation, discount display rules vary by provider and magnitude: PiggyCards and CTX handle discounts differently, and discounts less than 1% display to 1 decimal place while others display as whole numbers. This intentional differentiation exists in PiggyCardsRepository.calculateDisplayDiscount and DashSpendPayViewModel.updateMerchantInfo.
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/Models/**/*DAO.swift : Filter merchants by provider when fetching details to avoid duplicate/incorrect data from multi-provider database
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 739
File: DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift:152-251
Timestamp: 2025-11-24T15:33:27.646Z
Learning: In ExploreDatabaseConnection.swift test merchant setup, savingsPercentage values in both merchant and gift_card_providers tables must be consistent and expressed in basis points (e.g., 1000 = 10%). The values are divided by 10000 in DashSpendPayViewModel, so both tables should use the same basis point value for the same merchant to avoid discount calculation mismatches.
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 732
File: DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift:21-23
Timestamp: 2025-09-27T15:36:10.803Z
Learning: In PiggyCardsRepository.swift, there's an ACL mismatch (public static let shared in an internal class) and potential thread safety concerns that will be addressed during dedicated PiggyCards development rather than in the current UI improvements PR.
📚 Learning: 2025-09-27T15:36:10.803Z
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 732
File: DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift:21-23
Timestamp: 2025-09-27T15:36:10.803Z
Learning: In PiggyCardsRepository.swift, there's an ACL mismatch (public static let shared in an internal class) and potential thread safety concerns that will be addressed during dedicated PiggyCards development rather than in the current UI improvements PR.

Applied to files:

  • CLAUDE.md
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to **/CTX*.swift : Use blockchain transaction ID (txid), not gift card UUID, when fetching gift cards from CTX API

Applied to files:

  • CLAUDE.md
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: NEVER commit or push changes without explicit user permission in Git workflow

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-05T04:46:12.717Z
Learnt from: HashEngineering
Repo: dashpay/dashwallet-ios PR: 728
File: DashWallet.xcodeproj/project.pbxproj:1595-1599
Timestamp: 2025-09-05T04:46:12.717Z
Learning: In iOS projects, the `DashWallet.xcodeproj/project.pbxproj` file is automatically generated and managed by Xcode. Manual changes to this file should not be made, and the changes shown in diffs are typically the result of Xcode updating project configuration, dependencies, or build settings.

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m : Don't commit DWUpholdMainnetConstants.m if it only has whitespace changes from clang-format script; restore it instead

Applied to files:

  • CLAUDE.md
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-08-25T21:01:26.493Z
Learnt from: HashEngineering
Repo: dashpay/dashwallet-ios PR: 723
File: DashWallet/da.lproj/Localizable.strings:2987-2987
Timestamp: 2025-08-25T21:01:26.493Z
Learning: The DashWallet project uses Transifex for translation management rather than direct manual edits to .lproj/Localizable.strings files.

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/UI/**/*.swift : All new UI components MUST be built with SwiftUI, not UIKit ViewControllers or Storyboards

Applied to files:

  • CLAUDE.md
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: MCP servers must be configured in ~/Library/Application Support/Claude/claude_desktop_config.json and require Claude Code restart

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Figma Desktop App must be running with Dev Mode enabled (Shift+D) for Figma MCP server to work

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-11-25T20:39:25.847Z
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 737
File: DashWallet/Sources/Models/Explore Dash/Model/Entites/PiggyCardsModels.swift:200-235
Timestamp: 2025-11-25T20:39:25.847Z
Learning: In DashWallet iOS gift card implementation, discount display rules vary by provider and magnitude: PiggyCards and CTX handle discounts differently, and discounts less than 1% display to 1 decimal place while others display as whole numbers. This intentional differentiation exists in PiggyCardsRepository.calculateDisplayDiscount and DashSpendPayViewModel.updateMerchantInfo.

Applied to files:

  • CLAUDE.md
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to **/CTX*.swift : CTX gift card fetch endpoint returns different response structures: direct object in production, paginated in staging

Applied to files:

  • CLAUDE.md
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/Models/**/*DAO.swift : Always query git_card_providers table (not merchant table) for denomination type and provider-specific data

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-24T15:33:27.646Z
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 739
File: DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift:152-251
Timestamp: 2025-11-24T15:33:27.646Z
Learning: In ExploreDatabaseConnection.swift test merchant setup, savingsPercentage values in both merchant and gift_card_providers tables must be consistent and expressed in basis points (e.g., 1000 = 10%). The values are divided by 10000 in DashSpendPayViewModel, so both tables should use the same basis point value for the same merchant to avoid discount calculation mismatches.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/UI/**/HomeViewController*.swift : Shortcut bar displays different button combinations based on wallet state: four different states based on balance and passphrase verification

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
📚 Learning: 2025-09-27T23:33:40.458Z
Learnt from: HashEngineering
Repo: dashpay/dashwallet-ios PR: 732
File: DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsEndpoint.swift:25-32
Timestamp: 2025-09-27T23:33:40.458Z
Learning: PiggyCards API getGiftCard endpoint uses an invalid path "gift-cards/\(txid)" that doesn't exist on the server (marked with TODO comment). The correct path should likely be "orders" with query parameters following the CTX API pattern, since PiggyCards uses "orders" for purchases while CTX uses "gift-cards" for both purchase and retrieval operations.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/Models/**/*DAO.swift : Filter merchants by provider when fetching details to avoid duplicate/incorrect data from multi-provider database

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-24T15:33:27.646Z
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 739
File: DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift:152-251
Timestamp: 2025-11-24T15:33:27.646Z
Learning: In ExploreDatabaseConnection.swift test merchant setup, denominationsType values must use lowercase ('fixed', 'range', 'option') in both merchant and gift_card_providers INSERT statements to match downstream filtering logic. Using uppercase values like 'Fixed' will cause merchants to fail denomination type filtering.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to **/CTX*.swift : Handle CTX field name differences: staging uses 'userDiscount', production uses 'savingsPercentage' with fallback chain

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/Models/**/*DAO.swift : Expand rectangular bounds by 50% when used for circular radius searches to ensure consistency

Applied to files:

  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWallet/Sources/Models/**/*DAO.swift : Apply both rectangular bounds filtering (SQL optimization) AND circular distance filtering (accuracy) for location-based queries

Applied to files:

  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
📚 Learning: 2025-11-24T15:33:27.646Z
Learnt from: bfoss765
Repo: dashpay/dashwallet-ios PR: 739
File: DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift:152-251
Timestamp: 2025-11-24T15:33:27.646Z
Learning: In ExploreDatabaseConnection.swift test merchant insertion (DEBUG/Testflight builds), FTS trigger operations (DROP TRIGGER and CREATE TRIGGER statements) must be wrapped in a single SQLite transaction using `db.transaction { }` to ensure atomicity. If any operation fails between dropping and recreating triggers, the database would be left without FTS triggers for the session.

Applied to files:

  • DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift
  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
📚 Learning: 2025-11-26T19:32:01.721Z
Learnt from: CR
Repo: dashpay/dashwallet-ios PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T19:32:01.721Z
Learning: Applies to DashWalletTests/**/*.swift : Test coordinate edge cases thoroughly in unit tests for location-based features

Applied to files:

  • DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift
🧬 Code graph analysis (1)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift (3)
DashWallet/Sources/Models/Explore Dash/Services/BarcodeScanner.swift (2)
  • from (39-66)
  • downloadAndScan (100-123)
DashWallet/Sources/Models/Explore Dash/Model/DAO/GiftCardsDAO.swift (3)
  • get (81-94)
  • updateCardDetails (105-135)
  • updateBarcode (137-166)
DashWallet/Sources/Models/Explore Dash/Services/DashSpend/PiggyCards/PiggyCardsRepository.swift (1)
  • getOrderStatus (302-314)
🪛 markdownlint-cli2 (0.18.1)
CLAUDE.md

1582-1582: Bare URL used

(MD034, no-bare-urls)


1600-1600: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift

[Warning] 36-36: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 187-187: Prefer != nil over let _ =

(unused_optional_binding)


[Warning] 215-215: Prefer != nil over let _ =

(unused_optional_binding)


[Warning] 274-274: Function should have complexity 10 or less; currently complexity is 11

(cyclomatic_complexity)


[Warning] 291-291: Don't include vertical whitespace (empty line) after opening braces

(vertical_whitespace_opening_braces)

DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift

[Warning] 231-231: Don't include vertical whitespace (empty line) after opening braces

(vertical_whitespace_opening_braces)


[Warning] 464-464: Don't include vertical whitespace (empty line) after opening braces

(vertical_whitespace_opening_braces)


[Warning] 658-658: Don't include vertical whitespace (empty line) after opening braces

(vertical_whitespace_opening_braces)

DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift

[Error] 160-160: Prefer checking isEmpty over comparing count to zero

(empty_count)


[Warning] 256-256: Don't include vertical whitespace (empty line) before closing braces

(vertical_whitespace_closing_braces)

🔇 Additional comments (6)
CLAUDE.md (1)

1-2650: ⚠️ Critical Inconsistency: PR Objectives Don't Match File Content

The PR objectives describe a transaction caching fix (HomeViewModel.shared, DWCurrentNetworkDidChangeNotification, clearCachedData), but this file is extensive documentation covering PiggyCards integration, gift card barcode handling, and other features.

This represents either:

  1. The wrong file being reviewed
  2. Mismatched PR objectives
  3. Multiple unrelated changes bundled together

Before proceeding with review, please clarify:

  • Is CLAUDE.md the correct file for the "clear cached transactions when switching networks" PR?
  • If yes, should extensive PiggyCards/gift card documentation be in the same commit as the transaction caching fix?
DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift (1)

168-253: Transaction wrapping for FTS triggers is correctly implemented.

The FTS trigger operations (DROP and CREATE) are now properly wrapped in db.transaction { } ensuring atomicity. If any operation fails, the entire transaction rolls back, preventing the database from being left without FTS triggers.

DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (2)

38-61: Well-implemented helper function for sourceId extraction.

The extractSourceId helper correctly handles multiple type scenarios (String, Int64, Int) and edge cases (nil, empty, ""). This addresses the DRY principle violation from the past review.


632-664: Good use of parameterized query in allLocations.

The allLocations method correctly uses parameterized queries (WHERE merchantId = ?) with db.prepare(providersQuery, merchant.merchantId), preventing SQL injection. This pattern should be applied to the other provider queries for consistency.

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/GiftCardDetailsViewModel.swift (2)

41-45: PiggyCards repository wiring is consistent with existing CTX pattern

Using PiggyCardsRepository.shared alongside CTXSpendRepository.shared keeps the dependency style consistent with the rest of this view model. No issues here.


177-183: Good: stopping ticker now also clears long-polling UI state

Resetting hasBeenPollingForLongTime in stopTicker() avoids leaking the “code is being generated” state into subsequent polls, which matches user expectations.

Comment thread CLAUDE.md
Comment thread CLAUDE.md
bfoss765 and others added 2 commits December 5, 2025 18:18
Replace hardcoded string "DWCurrentNetworkDidChangeNotification" with
the bridged Objective-C constant NSNotification.Name.DWCurrentNetworkDidChange
to ensure the observer actually receives the notification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@HashEngineering HashEngineering left a comment

Choose a reason for hiding this comment

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

LGTM, though the base branch is set to master instead of the piggycards branch

@bfoss765 bfoss765 changed the base branch from master to feature/piggycards December 15, 2025 19:48
@bfoss765 bfoss765 merged commit 963a687 into feature/piggycards Dec 15, 2025
3 checks passed
@bfoss765 bfoss765 deleted the fix/PC-testnet-transactions branch December 15, 2025 19:49
@coderabbitai coderabbitai Bot mentioned this pull request Dec 17, 2025
5 tasks
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