Skip to content

Conversation

@bfoss765
Copy link
Contributor

@bfoss765 bfoss765 commented Sep 5, 2025

… display, and country notice

  • Add enabled field to ExplorePointOfUse.Merchant model for CTX API integration
  • Update subtitleForCTX to use denominationsType field and handle "min-max" as flexible amounts
  • Implement "Temporarily unavailable" state based on merchantInfo.enabled from CTX API
  • Update buy button state to respect CTX API enabled status with fallback to local active status
  • Add comprehensive debugging for merchant field investigation
  • Fix GameStop and other merchants to correctly show "Flexible amounts" instead of "Fixed amounts"
  • Add createCountryNotice function to display country-specific gift card usage information
  • Add gift card icon asset for improved UI consistency
  • Create iOS development patterns documentation for future sessions

🤖 Generated with Claude Code

Issue being fixed or feature implemented

What was done?

How Has This Been Tested?

Breaking Changes

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

    • Info button in merchant details with a types dialog.
    • Distance to merchants shown when location is available.
    • New gift card icon.
  • Enhancements

    • Map sheet drag behavior and “Show all locations” flow.
    • Uses current visible map bounds; default center radius set to 1 mile.
    • Cleaner phone/website handling; improved gift card and contact sections.
    • Merchant availability respected in gift card purchase flow.
    • Sync banner now shows helpful message.
  • Bug Fixes

    • Safer handling of missing coordinates/data and pagination bounds.
    • Prevented potential crashes in layout and URL/phone actions.
  • Documentation

    • Added iOS development and Swift safety guides; CLAUDE developer guide.
  • Chores

    • Version bump to 8.4.2.

… display, and country notice

- Add enabled field to ExplorePointOfUse.Merchant model for CTX API integration
- Update subtitleForCTX to use denominationsType field and handle "min-max" as flexible amounts
- Implement "Temporarily unavailable" state based on merchantInfo.enabled from CTX API
- Update buy button state to respect CTX API enabled status with fallback to local active status
- Add comprehensive debugging for merchant field investigation
- Fix GameStop and other merchants to correctly show "Flexible amounts" instead of "Fixed amounts"
- Add createCountryNotice function to display country-specific gift card usage information
- Add gift card icon asset for improved UI consistency
- Create iOS development patterns documentation for future sessions

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

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

Adds documentation and developer guides, bumps MARKETING_VERSION to 8.4.2, introduces a gift-card icon asset, extends merchant model with an optional enabled flag and fallback logic, refactors details and list UIs with map/drag behaviors and location integration, hardens map/annotation safety, threads filters/map bounds through flows, and adds merchant-availability checks in payments.

Changes

Cohort / File(s) Summary
Documentation: iOS agents & guides
./claude/agents/ios-development-patterns.md, ./claude/agents/ios-swift-safety-patterns.md, CLAUDE.md
Adds comprehensive architecture, patterns, and safety guides; introduces Claude developer document.
Version bump
DashWallet.xcodeproj/project.pbxproj
Updates MARKETING_VERSION from 8.4.1 to 8.4.2 across iOS/watchOS targets.
Assets
DashWallet/Resources/AppAssets.xcassets/gift-card-icon.imageset/Contents.json
Adds new universal SVG gift-card icon asset.
Model: Merchant enabled flag
DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift
Adds Merchant.enabled: Bool?, updates initializer, and introduces isEnabled(fallbackActive:) method.
Details controller: map and filters threading
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
Adds currentFilters/currentMapBounds, draggable sheet behavior, info button, URL/phone sanitization, map button visibility, details refresh on CTX updates, and passes filters/bounds to child flows.
Details views: location integration and layout
.../Details/Views/PointOfUseDetailsView.swift, .../Details/Views/AtmDetailsView.swift
Integrates location observing, reorganizes sections (gift card/contact info), adds grabber + pan, distance formatting; ATM view simplified to a single card with action buttons, removes location/bottom sections.
List controllers: pan refactor and param passing
.../List/ExplorePointOfUseListViewController.swift, .../List/MerchantListViewController.swift, .../List/AtmListViewController.swift
Refactors vertical pan logic and snapping thresholds, propagates currentFilters/currentMapBounds to details, minor copy fixes.
Map view safety and bounds
.../List/Views/ExploreMapView.swift
Sets default centerRadius to 1 mile, derives bounds from visibleMapRect, guards against nil coordinates when creating annotations.
Data providers: bounds/filters and safety
.../List/Model/AllMerchantLocationsDataProvider.swift, .../List/Model/MerchantsDataProvider.swift
Threads currentFilters/currentMapBounds, computes bounds via MapKit when needed, filters active locations, guards pagination when no bounds available.
All locations UI
.../List/AllMerchantLocationsViewController.swift
Accepts and stores currentFilters/currentMapBounds; hides segments section; instantiates data provider with new params.
Cells
.../List/Cells/MerchantItemCell.swift, .../List/Cells/AtmItemCell.swift
Computes distance safely with optionals/authorization; adjusts payment icon rendering; removes force unwraps.
Explore UI misc
.../List/ExploreSyncBannerView.swift, .../List/Filters/MerchantFiltersView.swift, .../List/Filters/Model/MerchantFiltersViewModel.swift, .../List/Filters/Model/PointOfUseListFiltersModel.swift, .../List/Filters/TerritoryPickerView.swift
Sets sync banner text; formatting tweaks; adds isLocationServiceEnabled to filters view model.
Payment view model
.../Views/DashSpend/DashSpendPayViewModel.swift
Adds isMerchantEnabled, blocks purchase when disabled, updates availability on merchant info fetch.
Dialog license
.../Info/MerchantTypesDialog.swift
Inserts MIT license header; no code changes.
Repo config
.gitignore
Stops ignoring CLAUDE.md.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant DetailsVC as PointOfUseDetailsViewController
  participant CTX as CTXSpendService
  participant UI as PointOfUseDetailsView

  User->>DetailsVC: Open merchant
  DetailsVC->>CTX: tryRefreshCtxToken + fetch merchant info
  CTX-->>DetailsVC: MerchantInfo(denominations, enabled?)
  DetailsVC->>DetailsVC: updatingMerchant(..., enabled)
  DetailsVC->>UI: refreshDetailsViewWithUpdatedMerchant
  UI-->>User: Updated details (enabled state considered)
  alt Error fetching
    CTX-->>DetailsVC: Error
    DetailsVC->>UI: Show existing data (no enabled) / fallback
  end
Loading
sequenceDiagram
  autonumber
  participant List as ExplorePointOfUseListVC
  participant Map as ExploreMapView
  participant Provider as AllMerchantLocationsDataProvider

  List->>Map: Display / pan map
  Map-->>List: didChangeVisibleBounds(bounds)
  List->>Provider: items(..., currentFilters, currentMapBounds=bounds)
  alt Location denied/needs auth
    Provider->>Provider: finalBounds=nil (global)
  else Bounds provided
    Provider->>Provider: Use currentMapBounds
  else Authorized with location
    Provider->>Provider: Compute MKCircle(rect from radius)
  end
  Provider-->>List: Paginated active locations
  List-->>List: Update UI list
Loading
sequenceDiagram
  autonumber
  actor User
  participant VM as DashSpendPayViewModel
  participant API as CTXSpendService

  User->>VM: purchaseGiftCardAndPay()
  VM->>VM: guard isMerchantEnabled
  alt Disabled
    VM-->>User: Error: merchantUnavailable
  else Enabled
    VM->>API: purchaseGiftCard(...)
    API-->>VM: Result
    VM-->>User: Success or error
  end

  Note over VM,API: updateMerchantInfo() updates isMerchantEnabled from API.enabled
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • HashEngineering
  • abaranouski

Poem

A hop, a bop, version ticks to two,
New maps that glide, with bounds in view.
A gift-card gleam, an icon bright,
Safe coords dance in gentle light.
If merchant sleeps, we pause the spend—
Then thump our tails and test again. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title is related to the changeset: it calls out a UI/design implementation and the hard-coded United States merchant support that appear in the diffs (country notice, gift-card asset, CTX enabled handling). The phrase "New design" is somewhat generic and oddly capitalized, but the title is concise and conveys a meaningful aspect of the PR. Overall it is informative enough for a quick scan by teammates.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improvement/merchant-country-info

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

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

Caution

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

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

281-286: Prevent purchases when merchant is disabled (defense-in-depth).

UI may block the button, but add a server-side guard in the flow too.

@@
     func purchaseGiftCardAndPay() async throws -> Data {
         isProcessingPayment = true
         defer { isProcessingPayment = false }
+        guard isMerchantEnabled else {
+            DSLogger.log("Purchase blocked: merchant disabled")
+            throw CTXSpendError.paymentProcessingError("Merchant temporarily unavailable")
+        }
@@
     private func purchaseGiftCardAPI() async throws -> GiftCardResponse {
-        guard !merchantId.isEmpty, ctxSpendService.isUserSignedIn else {
+        guard !merchantId.isEmpty, ctxSpendService.isUserSignedIn else {
             DSLogger.log("Purchase gift card failed: User not signed in or merchant ID is empty")
             throw CTXSpendError.unauthorized
         }
+        guard isMerchantEnabled else {
+            DSLogger.log("CTX API call blocked: merchant disabled")
+            throw CTXSpendError.paymentProcessingError("Merchant temporarily unavailable")
+        }

Also applies to: 166-186

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (1)

33-44: Crash risk: forced unwrap of coordinates.

pointOfUse.latitude! / longitude! can be nil; this will crash. Unwrap safely and hide the subtitle otherwise.

-            let distance = CLLocation(latitude: pointOfUse.latitude!, longitude: pointOfUse.longitude!)
-                .distance(from: currentLocation)
+            guard let lat = pointOfUse.latitude, let lon = pointOfUse.longitude else {
+                subLabel.isHidden = true
+                return
+            }
+            let distance = CLLocation(latitude: lat, longitude: lon)
+                .distance(from: currentLocation)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)

49-54: Prevent IUO crash when mapView is nil in viewDidLayoutSubviews

mapView?.contentInset = ... mapView.frame.height ... dereferences mapView even when it’s nil. Also guard detailsView.

 override func viewDidLayoutSubviews() {
   super.viewDidLayoutSubviews()
-
-  mapView?.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: mapView.frame.height - detailsView.frame.height - 10,
-                                       right: 0)
+  guard let mapView = mapView, let detailsView = detailsView else { return }
+  let bottomInset = max(0, mapView.frame.height - detailsView.frame.height - 10)
+  mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
 }

282-309: Nil-safe details view construction

detailsView(for:) returns optional but the code immediately dereferences detailsView. Use a guarded unwrap to avoid runtime crashes when category is .unknown.

-    detailsView = detailsView(for: pointOfUse)
-    detailsView.payWithDashHandler = payWithDashHandler
+    guard let builtDetailsView = detailsView(for: pointOfUse) else { return }
+    detailsView = builtDetailsView
+    builtDetailsView.payWithDashHandler = payWithDashHandler
-    detailsView.sellDashHandler = sellDashHandler
+    builtDetailsView.sellDashHandler = sellDashHandler
-    detailsView.showAllLocationsActionBlock = { [weak self] in
+    builtDetailsView.showAllLocationsActionBlock = { [weak self] in
 ...
-    detailsView.buyGiftCardHandler = { [weak self] in
+    builtDetailsView.buyGiftCardHandler = { [weak self] in
 ...
-    detailsView.dashSpendAuthHandler = { [weak self] in
+    builtDetailsView.dashSpendAuthHandler = { [weak self] in
 ...
-    detailsView.translatesAutoresizingMaskIntoConstraints = false
-    contentView.addSubview(detailsView)
+    builtDetailsView.translatesAutoresizingMaskIntoConstraints = false
+    contentView.addSubview(builtDetailsView)
 ...
-            detailsView.topAnchor.constraint(equalTo: contentView.topAnchor),
+            builtDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor),
 ...
-            detailsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
-            detailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+            builtDetailsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+            builtDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

176-180: Harden websiteAction URL handling

Add https:// when the merchant URL is missing a scheme.

 @objc
 func websiteAction() {
-    guard let website = merchant.website, let url = URL(string: website) else { return }
+    guard var website = merchant.website, !website.isEmpty else { return }
+    if !website.lowercased().hasPrefix("http") { website = "https://\(website)" }
+    guard let url = URL(string: website) else { return }
     UIApplication.shared.open(url, options: [:], completionHandler: nil)
 }
🧹 Nitpick comments (19)
DashWallet/Resources/AppAssets.xcassets/gift-card-icon.imageset/Contents.json (1)

1-12: Set icon as template + preserve vector; confirm minimum iOS support for SVG assets.

  • If you intend to tint this icon, mark it as a template image and preserve vector data.
  • Ensure the deployment target is iOS 13+ (SVG support in asset catalogs starts there). Otherwise, consider raster fallbacks. (sarunw.com, avanderlee.com)

Apply this diff:

 {
   "images" : [
     {
       "filename" : "gift-card-icon.svg",
       "idiom" : "universal"
     }
   ],
+  "properties" : {
+    "template-rendering-intent" : "template",
+    "preserves-vector-representation" : true
+  },
   "info" : {
     "author" : "xcode",
     "version" : 1
   }
 }
DashWallet.xcodeproj/project.pbxproj (1)

10356-10356: Centralize MARKETING_VERSION to avoid drift across targets.

Consider moving MARKETING_VERSION to a shared .xcconfig (e.g., Config/Version.xcconfig) and referencing it from all targets to keep them in lockstep.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (1)

89-90: Unify default radius across list and map

List controller reportedly uses 5 miles; this view now uses 1 mile. Pick one and centralize (e.g., ExploreMapConstants.defaultMiles) to avoid UX whiplash.

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

64-70: Button height consistency

Only payButton has an explicit height. When both buttons are present, they should share height. Either constrain both to 48 or tie sellButton’s height to payButton’s.

Example patch (after creating sellButton):

sellButton.heightAnchor.constraint(equalTo: payButton.heightAnchor).isActive = true

28-28: Trim trailing whitespace flagged by SwiftLint

Minor cleanliness.

Also applies to: 30-30, 36-36, 40-40, 63-63

.claude/agents/ios-development-patterns.md (4)

143-164: SwiftUI snippet conflicts with UIKit reality

The doc shows struct PointOfUseDetailsView: View with Combine/SwiftUI patterns, but in code PointOfUseDetailsView is a UIKit class. Clarify that this is pseudo‑code or provide a UIKit‑accurate example to avoid confusion.


450-458: Invalid Swift sample: nil-coalescing Bool? with String

The prints use ?? "nil" on Bool?, which won’t compile. Use String mapping.

#if DEBUG
extension MerchantViewModel {
    func debugDataFlow() {
        print("🎯 CTX Data: \(ctxMerchant?.enabled.map(String.init) ?? "nil")")
        print("🎯 Local Data: \(localMerchant?.acceptsDash.map(String.init) ?? "nil")")
        print("🎯 Final Value: \((merchant?.safeEnabled).map(String.init) ?? "nil")")
    }
}
#endif

499-509: Migration snippet: add IF NOT EXISTS and idempotency notes

To make the example safer to re-run, include IF NOT EXISTS and guard the UPDATE.

ALTER TABLE IF NOT EXISTS merchant ADD COLUMN enabled BOOLEAN DEFAULT 1;

-- Only backfill when the column exists and value is NULL
UPDATE merchant
SET enabled = acceptsDash
WHERE enabled IS NULL;

Add a reminder in the doc to wrap migrations in a recorded “applied” table/flag.


63-91: Sample accessors reference undefined properties

safeEnabled uses ctxEnabled/localEnabled that aren’t on Merchant in the sample. Either inline the resolution or show where these come from to prevent copy/paste errors.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2)

26-26: Remove the unused distanceLabel or make it non-IUO.

You render distance via subLabel and immediately hide distanceLabel. Either remove it (preferred) or avoid IUO (UILabel!) per SwiftLint.

Option A (remove):

-    private var distanceLabel: UILabel!
+    // distanceLabel removed; subLabel is the canonical distance UI
@@
-        // Hide separate distance label since we're using subLabel
-        distanceLabel.isHidden = true
@@
-        // Distance label (separate from merchant name)
-        distanceLabel = UILabel()
-        distanceLabel.translatesAutoresizingMaskIntoConstraints = false
-        distanceLabel.font = .systemFont(ofSize: 12, weight: .regular)
-        distanceLabel.textColor = .dw_tertiaryText()
-        distanceLabel.isHidden = true
-        distanceLabel.textAlignment = .right
-        mainStackView.addArrangedSubview(distanceLabel)

Option B (keep, but safe):

-    private var distanceLabel: UILabel!
+    private var distanceLabel = UILabel()
@@
-        distanceLabel = UILabel()
         distanceLabel.translatesAutoresizingMaskIntoConstraints = false

Also applies to: 46-47, 66-74


45-45: Trim trailing whitespace.

DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (1)

64-76: Clarify enabled semantics and add helper for fallback logic.

Document that CTX may omit enabled and provide a helper to merge with local active.

         let redeemType: String?
-        let enabled: Bool?
+        /// CTX "enabled" flag. If nil, the state is unknown (fallback to local "active").
+        let enabled: Bool?
@@
-        init(merchantId: String, paymentMethod: PaymentMethod, type: `Type`, deeplink: String?, savingsBasisPoints: Int, denominationsType: String?, denominations: [Int] = [], redeemType: String?, enabled: Bool? = nil) {
+        init(
+            merchantId: String,
+            paymentMethod: PaymentMethod,
+            type: `Type`,
+            deeplink: String?,
+            savingsBasisPoints: Int,
+            denominationsType: String?,
+            denominations: [Int] = [],
+            redeemType: String?,
+            enabled: Bool? = nil
+        ) {
@@
             self.redeemType = redeemType
             self.enabled = enabled
         }
+
+        /// Effective enabled status given local "active" fallback.
+        func isEnabled(fallbackActive: Bool) -> Bool {
+            enabled ?? fallbackActive
+        }

Also addresses the SwiftLint long-line warning.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)

25-26: “Max 50% of screen height” comment mismatches constant. Consider dynamic value.

-internal let kDefaultOpenedMapPosition: CGFloat = 320.0 // Max 50% of screen height
+internal var kDefaultOpenedMapPosition: CGFloat { min(UIScreen.main.bounds.height * 0.5, 320.0) } // Cap at 50% or 320pt

532-579: Improve pan UX: update contentInset while dragging and clamp consistently.

Currently, you allow dragging to 80% height but snap to 320 on end; update inset during .changed for visual consistency and optionally clamp to the same range you’ll snap to.

 case .changed:
-    // Only handle vertical movement and constrain within bounds
-    let newY = currentY + translatedPoint.y
-
-    // Constrain movement between closed position and maximum expanded position
-    let maxY = view.frame.size.height * 0.8 // Allow dragging down to 80% of screen
-    let minY = kDefaultClosedMapPosition // Don't allow dragging above closed position
-
-    contentViewTopLayoutConstraint.constant = max(minY, min(maxY, newY))
+    let newY = currentY + translatedPoint.y
+    // Constrain between closed and opened positions for predictable snapping
+    let minY = kDefaultClosedMapPosition
+    let maxY = kDefaultOpenedMapPosition
+    let clampedY = max(minY, min(maxY, newY))
+    contentViewTopLayoutConstraint.constant = clampedY
+    // Keep map inset in sync while dragging
+    self.mapView.contentInset = .init(top: 0, left: 0, bottom: self.mapView.frame.height - clampedY, right: 0)
     sender.setTranslation(.zero, in: view)

If you do want 80% drag freedom, consider snapping thresholds that include that range or animate to that expanded value too.


567-570: Parameter alignment + trailing whitespace (SwiftLint).

-            UIView.animate(withDuration: animationDuration, delay: 0, 
-                          usingSpringWithDamping: 0.8, initialSpringVelocity: 0, 
-                          options: .curveEaseOut) {
+            UIView.animate(withDuration: animationDuration, delay: 0,
+                           usingSpringWithDamping: 0.8, initialSpringVelocity: 0,
+                           options: .curveEaseOut) {

Also trim trailing spaces on nearby lines 540, 544, 567–568.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

179-223: Remove dead Map button code (unused + potential nil usage)

showMapButton and related methods are no longer wired; keeping them risks accidental calls against an uninitialized button. Recommend removing the property and these methods.

-    private func setupMapButton() { ... }
-    @objc
-    private func showMapAction() { ... }
-    private func updateMapButtonVisibility() { ... }

Also remove private var showMapButton: UIButton! at Line 36.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)

789-811: Country notice is hard-coded to US

If this should only show for US merchants/users, add a condition (e.g., merchant country or device locale). If “US-only” is intentional for this release, ignore.

Would you like a quick follow-up patch to gate this on a country check?


594-600: Replace placeholder location count

getLocationCount() returns a hard-coded 11. Wire this up to the real data source and pass it from the VC.

I can add a small API on the VC to inject the count and remove this placeholder.


25-51: Reduce IUOs for view properties

grabberContainer, logoImageView, etc. as IUOs are fragile. Prefer optional or lazy let where possible.

If desired, I can prepare a small refactor to remove IUOs without changing behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e109e15 and 973cbbd.

⛔ Files ignored due to path filters (1)
  • DashWallet/Resources/AppAssets.xcassets/gift-card-icon.imageset/gift-card-icon.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • .claude/agents/ios-development-patterns.md (1 hunks)
  • DashWallet.xcodeproj/project.pbxproj (12 hunks)
  • DashWallet/Resources/AppAssets.xcassets/gift-card-icon.imageset/Contents.json (1 hunks)
  • DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (2 hunks)
  • DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (10 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (3 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-05T04:46:12.717Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#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:

  • DashWallet/Resources/AppAssets.xcassets/gift-card-icon.imageset/Contents.json
  • DashWallet.xcodeproj/project.pbxproj
📚 Learning: 2025-08-25T21:01:26.493Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#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:

  • DashWallet.xcodeproj/project.pbxproj
📚 Learning: 2025-09-05T04:46:21.894Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#728
File: DashWallet.xcodeproj/project.pbxproj:3174-3180
Timestamp: 2025-09-05T04:46:21.894Z
Learning: Xcode project files (.pbxproj) are automatically generated by Xcode and should not be manually edited. Hash changes in Pod library references (like libPods-dashwallet.a) are normal and occur when CocoaPods updates or when Xcode regenerates the project structure.

Applied to files:

  • DashWallet.xcodeproj/project.pbxproj
🧬 Code graph analysis (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (6)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (1)
  • stopNetworkMonitoring (52-54)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)
  • callAction (84-99)
  • payAction (79-82)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (1)
  • configureGiftCardSection (22-71)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)
  • locationManagerDidChangeCurrentLocation (283-285)
  • locationManagerDidChangeCurrentReversedLocation (295-295)
  • locationManagerDidChangeServiceAvailability (287-293)
DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (1)
  • toSavingPercentages (78-80)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift (1)
  • update (41-49)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (1)
  • update (28-48)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (2)
  • configureGiftCardSection (276-286)
  • createHeaderSection (658-728)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (3)
  • setupInfoButton (272-276)
  • configureHierarchy (232-238)
  • infoButtonAction (278-283)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)
  • configureHierarchy (203-226)
  • callAction (130-158)
  • setupGrabberPanGesture (1084-1095)
DashWallet/Sources/Categories/UIHostingController+DashWallet.swift (1)
  • setDetent (21-31)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (3)
  • mapView (239-244)
  • mapView (246-256)
  • mapView (272-278)
🪛 LanguageTool
.claude/agents/ios-development-patterns.md

[grammar] ~9-~9: There might be a mistake here.
Context: ... #### Local vs Remote Database Confusion Critical Understanding: There are two ...

(QB_NEW_EN)


[grammar] ~12-~12: There might be a mistake here.
Context: ...ems: 1. Local Database (explore.db) - SQLite database stored locally on device...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...SQLite database stored locally on device - Contains cached merchant data and user p...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...ached merchant data and user preferences - Managed through `DatabaseConnection.swif...

(QB_NEW_EN)


[grammar] ~15-~15: There might be a mistake here.
Context: ...d user preferences - Managed through DatabaseConnection.swift - Used for offline functionality and quick...

(QB_NEW_EN)


[grammar] ~18-~18: There might be a mistake here.
Context: ...base.appspot.com/explore/explore-v3.db`)** - Firebase-hosted SQLite database - Sou...

(QB_NEW_EN)


[grammar] ~19-~19: There might be a mistake here.
Context: ...)** - Firebase-hosted SQLite database - Source of truth for merchant information...

(QB_NEW_EN)


[grammar] ~20-~20: There might be a mistake here.
Context: ...Source of truth for merchant information - Downloaded and synced periodically - ...

(QB_NEW_EN)


[grammar] ~21-~21: There might be a mistake here.
Context: ... - Downloaded and synced periodically - Contains the complete merchant directory...

(QB_NEW_EN)


[grammar] ~53-~53: There might be a mistake here.
Context: ...e` (represents payment type flexibility) - Always check for both old and new field ...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ...ft card vendors (GameStop, Target, etc.) - merchant: Main merchant directory table - **Rela...

(QB_NEW_EN)


[grammar] ~58-~58: There might be a mistake here.
Context: ...merchant`: Main merchant directory table - Relationship: Some merchants may be li...

(QB_NEW_EN)


[grammar] ~196-~196: There might be a mistake here.
Context: ...tions #### Required Initializer Updates Rule: When adding properties to Swift ...

(QB_NEW_EN)


[grammar] ~319-~319: There might be a mistake here.
Context: ...itfall 1: Confusing Local vs Remote Data Problem: Querying wrong database or as...

(QB_NEW_EN)


[grammar] ~320-~320: There might be a mistake here.
Context: ...using Local vs Remote Data Problem: Querying wrong database or assuming data exists ...

(QB_NEW_EN)


[grammar] ~320-~320: There might be a mistake here.
Context: ...atabase or assuming data exists locally. Solution: ```swift // Always specif...

(QB_NEW_EN)


[grammar] ~488-~488: There might be a mistake here.
Context: ...ion #### 1. Property Addition Checklist - [ ] Add property to model struct/class -...

(QB_NEW_EN)


[grammar] ~489-~489: There might be a mistake here.
Context: ...- [ ] Add property to model struct/class - [ ] Update all initializers with default...

(QB_NEW_EN)


[grammar] ~490-~490: There might be a mistake here.
Context: ...ate all initializers with default values - [ ] Update database schema if needed - [...

(QB_NEW_EN)


[grammar] ~491-~491: There might be a mistake here.
Context: ...s - [ ] Update database schema if needed - [ ] Add migration script for existing da...

(QB_NEW_EN)


[grammar] ~492-~492: There might be a mistake here.
Context: ...] Add migration script for existing data - [ ] Update API serialization/deserializa...

(QB_NEW_EN)


[grammar] ~493-~493: There might be a mistake here.
Context: ...Update API serialization/deserialization - [ ] Add unit tests for new property - [ ...

(QB_NEW_EN)


[grammar] ~494-~494: There might be a mistake here.
Context: ...on - [ ] Add unit tests for new property - [ ] Update UI components that use the mo...

(QB_NEW_EN)


[grammar] ~534-~534: There might be a mistake here.
Context: ...Points ### CTXSpendService Architecture - Location: `DashWallet/Sources/Models/S...

(QB_NEW_EN)


[grammar] ~535-~535: There might be a mistake here.
Context: ...endService Architecture - Location: DashWallet/Sources/Models/Services/CTXSpendService.swift - Purpose: Handles communication with CT...

(QB_NEW_EN)


[grammar] ~537-~537: There might be a mistake here.
Context: ...API for merchant data - Key Methods: - refreshTokenAndMerchantInfo(): Updates merchant data from CTX - `ge...

(QB_NEW_EN)


[grammar] ~542-~542: There might be a mistake here.
Context: ... ### PointOfUseDetailsView Architecture - Location: `DashWallet/Sources/UI/Explo...

(QB_NEW_EN)


[grammar] ~543-~543: There might be a mistake here.
Context: ...etailsView Architecture - Location: DashWallet/Sources/UI/Explore/PointOfUseDetailsView.swift - Purpose: Displays detailed merchant in...

(QB_NEW_EN)


[grammar] ~545-~545: There might be a mistake here.
Context: ...dService → updatingMerchant → UI refresh - Key Pattern: Combine publishers trigge...

(QB_NEW_EN)


[grammar] ~548-~548: There might be a mistake here.
Context: ...es ### ExplorePointOfUse.Merchant Model - Location: Model definitions for mercha...

(QB_NEW_EN)


[grammar] ~549-~549: There might be a mistake here.
Context: ...definitions for merchant data structures - Evolution: Handles both legacy and cur...

(QB_NEW_EN)


[grammar] ~550-~550: There might be a mistake here.
Context: ...dles both legacy and current field names - Integration: Works with both CTX API a...

(QB_NEW_EN)


[grammar] ~555-~555: There might be a mistake here.
Context: ...ctices Summary ### Development Workflow 1. Always verify data sources before impl...

(QB_NEW_EN)


[grammar] ~562-~562: There might be a mistake here.
Context: ...rty addition checklist ### Code Quality 1. Prefer explicit over implicit when dea...

(QB_NEW_EN)

🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 80-80: Force unwrapping should be avoided

(force_unwrapping)


[Warning] 81-81: Line should be 120 characters or less; currently it has 159 characters

(line_length)


[Warning] 82-82: Line should be 120 characters or less; currently it has 154 characters

(line_length)


[Warning] 84-84: Line should be 120 characters or less; currently it has 156 characters

(line_length)


[Warning] 78-78: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 130-130: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 133-133: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 138-138: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 142-142: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 143-143: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 145-145: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 147-147: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 153-153: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 159-159: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 205-205: Prefer object literals over image and color inits

(object_literal)


[Warning] 206-206: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 209-209: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 276-276: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 382-382: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 434-434: Line should be 120 characters or less; currently it has 181 characters

(line_length)


[Warning] 547-547: Line should be 120 characters or less; currently it has 125 characters

(line_length)


[Warning] 617-617: Line should be 120 characters or less; currently it has 126 characters

(line_length)


[Warning] 350-350: Prefer object literals over image and color inits

(object_literal)


[Warning] 481-481: Prefer object literals over image and color inits

(object_literal)


[Warning] 547-547: Prefer object literals over image and color inits

(object_literal)


[Warning] 247-247: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 250-250: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 255-255: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 259-259: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 261-261: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 265-265: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 269-269: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 278-278: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 287-287: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 295-295: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 303-303: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 307-307: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 315-315: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 323-323: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 331-331: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 335-335: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 339-339: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 343-343: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 352-352: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 363-363: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 366-366: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 374-374: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 377-377: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 389-389: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 395-395: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 402-402: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 408-408: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 414-414: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 421-421: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 429-429: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 439-439: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 445-445: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 448-448: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 454-454: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 459-459: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 464-464: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 468-468: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 473-473: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 478-478: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 484-484: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 488-488: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 491-491: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 494-494: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 501-501: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 504-504: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 510-510: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 519-519: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 527-527: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 530-530: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 534-534: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 539-539: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 544-544: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 551-551: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 554-554: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 556-556: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 564-564: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 567-567: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 570-570: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 574-574: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 586-586: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 590-590: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 593-593: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 600-600: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 604-604: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 609-609: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 614-614: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 623-623: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 626-626: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 628-628: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 636-636: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 639-639: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 642-642: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 654-654: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 657-657: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 661-661: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 668-668: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 677-677: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 683-683: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 692-692: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 701-701: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 708-708: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 711-711: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 714-714: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 716-716: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 726-726: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 729-729: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 733-733: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 739-739: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 746-746: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 751-751: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 756-756: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 759-759: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 763-763: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 766-766: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 775-775: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 777-777: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 785-785: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 788-788: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 792-792: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 800-800: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 802-802: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 809-809: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 816-816: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 814-814: Limit vertical whitespace to a single empty line; currently 3

(vertical_whitespace)


[Warning] 908-908: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 947-947: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 981-981: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 959-959: Prefer object literals over image and color inits

(object_literal)


[Warning] 869-869: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 872-872: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 874-874: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 889-889: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 892-892: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 898-898: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 906-906: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 909-909: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 916-916: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 919-919: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 925-925: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 933-933: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 940-940: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 945-945: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 948-948: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 951-951: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 954-954: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 961-961: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 966-966: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 968-968: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 974-974: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 979-979: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 982-982: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 986-986: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 989-989: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 996-996: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1010-1010: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1014-1014: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1021-1021: Lines should not have trailing whitespace

(trailing_whitespace)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1054-1054: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1063-1063: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1069-1069: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1075-1075: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1082-1082: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1083-1083: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1085-1085: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1087-1087: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1096-1096: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 1083-1083: Limit vertical whitespace to a single empty line; currently 2

(vertical_whitespace)


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

(vertical_whitespace_closing_braces)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift

[Warning] 26-26: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift

[Error] 66-66: Line should be 200 characters or less; currently it has 220 characters

(line_length)


[Warning] 65-65: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift

[Warning] 28-28: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 30-30: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 36-36: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 40-40: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 63-63: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 36-36: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 67-67: Line should be 120 characters or less; currently it has 121 characters

(line_length)


[Warning] 68-68: Line should be 120 characters or less; currently it has 124 characters

(line_length)


[Warning] 160-160: Line should be 120 characters or less; currently it has 136 characters

(line_length)


[Warning] 210-210: Line should be 120 characters or less; currently it has 132 characters

(line_length)


[Warning] 71-71: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 78-78: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 90-90: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 94-94: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 100-100: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 104-104: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 106-106: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 108-108: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 111-111: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 116-116: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 125-125: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 128-128: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 137-137: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 139-139: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 150-150: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 159-159: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 167-167: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 178-178: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 192-192: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 195-195: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 203-203: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 209-209: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 214-214: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 217-217: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 264-264: Force unwrapping should be avoided

(force_unwrapping)


[Warning] 260-260: Line should be 120 characters or less; currently it has 127 characters

(line_length)


[Warning] 261-261: Line should be 120 characters or less; currently it has 134 characters

(line_length)


[Warning] 256-256: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 262-262: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 309-309: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift

[Warning] 540-540: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 544-544: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 567-567: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 568-568: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 568-568: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 569-569: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)

🔇 Additional comments (19)
DashWallet.xcodeproj/project.pbxproj (11)

10496-10496: LGTM: Version bump is consistent.


10670-10670: LGTM: WatchApp target reflects 8.4.2.


10692-10692: LGTM: WatchApp target (other config) updated too.


10717-10717: LGTM: WatchKit extension updated.


10744-10744: LGTM: WatchKit extension (other config) updated.


11484-11484: LGTM: iOS target config shows 8.4.2.


11653-11653: LGTM: WatchApp config aligned.


11678-11678: LGTM: WatchKit extension config aligned.


11795-11795: LGTM: iOS target config aligned.


11962-11962: LGTM: WatchApp config aligned.


11987-11987: LGTM: WatchKit extension config aligned.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (1)

22-41: Method naming/context check

Overriding configureGiftCardSection() for an ATM view reads oddly. Confirm the superclass actually calls this for ATM flows (vs a header/location section hook) to avoid dead code.

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

58-58: Confirm DTO optionality and fallback seeding
I couldn’t locate ExplorePointOfUse or the type of merchantInfo.enabled in the codebase—please confirm that merchant.merchant?.enabled is an optional Bool (and if not, simplify to direct assignment) and seed isMerchantEnabled from merchant.active until the CTX fetch completes.

DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (1)

250-252: Confirm DB persistence for enabled in ExplorePointOfUse
The RowDecodable initializer never calls row.get(ExplorePointOfUse.enabled) (the property is declared at line 64 but only set to its default), so enabled always remains nil and falls back to .active. If you intend to persist this flag, add the enabled column, update the decoder with row.get(ExplorePointOfUse.enabled) and migrate the DB; otherwise remove or ignore it.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

466-480: Merchant.enabled propagation looks correct

The updated updatingMerchant correctly carries over all existing fields and overrides enabled when provided.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)

865-871: Button enabling logic aligns with PR goals

isEnabled = m.enabled ?? merchant.active with network and sync gating matches “Temporarily unavailable” behavior.


658-726: Header layout looks solid

Good use of content hugging/compression to prevent truncation and maintain logo sizing.


730-787: CTX section copy and discount formatting look correct

“Temporarily unavailable” path and savings percentage rendering are consistent with model fields.


1046-1095: Grabber implementation is clean and reusable

Good separation and constraints; pairs nicely with controller’s pan handler.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

408-416: UI not refreshed after CTX merchant update; details view remains stale.

The updated enabled/denominations from CTX are not applied to the on-screen PointOfUseDetailsView created earlier.

Minimal fix: rebuild details view after updating pointOfUse.

             if try await tryRefreshCtxToken(), let merchantId = pointOfUse.merchant?.merchantId {
                 let merchantInfo = try await CTXSpendService.shared.getMerchant(merchantId: merchantId)
                 pointOfUse = pointOfUse.updatingMerchant(
                     denominationsType: merchantInfo.denominationsType,
                     denominations: merchantInfo.denominations.compactMap { Int($0) },
                     enabled: merchantInfo.enabled
                 )
+                await MainActor.run {
+                    self.rebuildDetailsView(with: self.pointOfUse)
+                }
             }

Add this helper (outside the changed hunk):

private func rebuildDetailsView(with pou: ExplorePointOfUse) {
    detailsView.removeFromSuperview()
    detailsView = detailsView(for: pou)
    detailsView.payWithDashHandler = payWithDashHandler
    detailsView.sellDashHandler = sellDashHandler
    detailsView.buyGiftCardHandler = { [weak self] in self?.showDashSpendPayScreen() }
    detailsView.dashSpendAuthHandler = { [weak self] in self?.showCTXSpendLoginInfo() }
    detailsView.translatesAutoresizingMaskIntoConstraints = false
    contentView.addSubview(detailsView)
    NSLayoutConstraint.activate([
        detailsView.topAnchor.constraint(equalTo: contentView.topAnchor),
        detailsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
        detailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
    ])
}
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

176-180: Normalize website URL (trim + scheme) before opening.

Matches controller behavior; handles scheme-less inputs.

-        guard let website = merchant.website, let url = URL(string: website) else { return }
+        guard var website = merchant.website?.trimmingCharacters(in: .whitespacesAndNewlines), !website.isEmpty else { return }
+        if !website.lowercased().hasPrefix("http") { website = "https://" + website }
+        guard let url = URL(string: website) else { return }
         UIApplication.shared.open(url, options: [:], completionHandler: nil)
♻️ Duplicate comments (3)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)

173-186: Harden websiteAction URL normalization (trim + scheme check, case-insensitive).

Handles uppercase schemes/whitespace; avoids invalid URLs.

Apply:

-        guard let website = pointOfUse.website else { return }
-        
-        // Normalize URL by adding https scheme if missing
-        let normalizedWebsite: String
-        if website.hasPrefix("http://") || website.hasPrefix("https://") {
-            normalizedWebsite = website
-        } else {
-            normalizedWebsite = "https://" + website
-        }
-        
-        guard let url = URL(string: normalizedWebsite) else { return }
+        guard var website = pointOfUse.website?.trimmingCharacters(in: .whitespacesAndNewlines), !website.isEmpty else { return }
+        if !website.lowercased().hasPrefix("http") { website = "https://" + website }
+        guard let url = URL(string: website) else { return }

267-275: Remove force unwrap of contentViewTopConstraint.

Prevents crash; aligns with prior lint feedback.

-            // Create the top constraint and store reference to it
-            // Start higher than bottom half by about an inch (72 points)
-            let screenHeight = UIScreen.main.bounds.height
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Higher by about an inch - 72 // Higher by about an inch
-            contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
-            
-            constraint = [
-                contentViewTopConstraint!,
+            // Create the top constraint and store reference to it
+            let screenHeight = UIScreen.main.bounds.height
+            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72
+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            contentViewTopConstraint = topConstraint
+
+            constraint = [
+                topConstraint,
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

130-158: Gate verbose phone debug prints behind DEBUG.

Prevents noisy prod logs and potential PII leakage.

-        print("DEBUG: Original phone: \(phone)")
+        #if DEBUG
+        print("DEBUG: Original phone: \(phone)")
+        #endif
...
-        print("DEBUG: Extracted digits: \(digits)")
+        #if DEBUG
+        print("DEBUG: Extracted digits: \(digits)")
+        #endif
...
-        print("DEBUG: URL string: \(urlString)")
+        #if DEBUG
+        print("DEBUG: URL string: \(urlString)")
+        #endif
...
-            print("DEBUG: Failed to create URL")
+            #if DEBUG
+            print("DEBUG: Failed to create URL")
+            #endif
...
-            print("DEBUG: Cannot open URL - phone not available")
+            #if DEBUG
+            print("DEBUG: Cannot open URL - phone not available")
+            #endif
...
-        print("DEBUG: Opening URL: \(url)")
+        #if DEBUG
+        print("DEBUG: Opening URL: \(url)")
+        #endif
-        UIApplication.shared.open(url, options: [:], completionHandler: { success in
-            print("DEBUG: URL open result: \(success)")
-        })
+        UIApplication.shared.open(url, options: [:], completionHandler: { success in
+            #if DEBUG
+            print("DEBUG: URL open result: \(success)")
+            #endif
+        })
🧹 Nitpick comments (11)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendEndpoint.swift (1)

83-89: Method choice and auth look correct; consider avoiding token refresh for logout.

POST + Bearer is standard. Optional: ensure the 401-refresh retry in CTXSpendAPI does not refresh tokens for .logout (users typically don’t want a refresh during sign-out). If desired, special-case .logout in CTXSpendAPI’s unauthorized handler.

// In CTXSpendAPI.request overrides, early-return on .logout for 401 without refresh
if case .logout = target { throw HTTPClientError.statusCode(r) }
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (2)

91-107: UX: sign-out state is delayed until the network call returns.

If the UI should reflect sign-out immediately, optimistically clear auth state first (or expose a completion to allow views to await). Otherwise, show a brief progress indicator during server revoke.

 func logout() {
-        // Attempt to revoke token on server before clearing local storage
-        Task {
+        // Option A: Optimistic local sign-out, then best-effort server revoke
+        updateSignInState() // if you also delete tokens here, move the block below
+        Task {
             do {
                 try await CTXSpendAPI.shared.request(.logout)
             } catch {
                 DSLogger.log("Logout API call failed: \(error). Proceeding with local cleanup.")
             }
             
             // Always clear local tokens regardless of server response
             await MainActor.run {
                 KeychainService.delete(key: Keys.accessToken)
                 KeychainService.delete(key: Keys.refreshToken)
                 KeychainService.delete(key: Keys.email)
                 userDefaults.removeObject(forKey: Keys.deviceUUID)
                 updateSignInState()
             }
         }
 }

98-98: Fix SwiftLint trailing whitespace (line 98).

Remove the trailing spaces on the blank line to satisfy trailing_whitespace.

-            
+
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)

103-109: Avoid shadowing property; gate debug prints; minor cleanups.

Improves readability and removes prod logs.

-        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
-        guard let contentViewTopConstraint = contentViewTopConstraint else { 
-            print("DEBUG: contentViewTopConstraint is nil!")
+        #if DEBUG
+        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
+        guard let topConstraint = contentViewTopConstraint else {
+            #if DEBUG
+            print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
             return 
         }
-        let translatedPoint: CGPoint = sender.translation(in: view)
-        let currentY = contentViewTopConstraint.constant
+        let translatedPoint: CGPoint = sender.translation(in: view)
+        let currentY = topConstraint.constant
...
-            contentViewTopConstraint.constant = max(minY, min(maxY, newY))
+            topConstraint.constant = max(minY, min(maxY, newY))
...
-            let finalCurrentY = contentViewTopConstraint.constant
+            let finalCurrentY = topConstraint.constant
...
-                contentViewTopConstraint.constant = finalY
+                topConstraint.constant = finalY

Also applies to: 129-166


214-226: Unify “closed map” position with pan gesture constants.

Avoids inconsistent sheet positions.

-        // Animate to show more map (closed position)
-        let kDefaultClosedMapPosition: CGFloat = 100.0
+        // Animate to show more map (closed position)
+        let screenHeight = view.frame.size.height
+        let kDefaultClosedMapPosition = screenHeight * 0.75

189-213: Remove dead Map button code or ensure it is always initialized.

Current IUO showMapButton is never set up; if any caller triggers updateMapButtonVisibility it would crash.

If unused, delete setupMapButton/showMapAction/updateMapButtonVisibility and the showMapButton property. Otherwise, call setupMapButton() when showMap is true.

Also applies to: 225-233, 320-326

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)

382-421: Contact info section composition LGTM; but replace placeholder location count.

UI structure looks good. Remove hardcoded getLocationCount() once data is wired.

Would you like a small protocol to inject location count from the VC?

Also applies to: 422-462


730-787: Gate merchant field debug logs behind DEBUG in subtitleForCTX.

Avoids leaking merchant metadata in prod logs.

-        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") {
+        #if DEBUG
+        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") {
             print("🎯 MERCHANT DEBUG: \(self.merchant.name)")
             ...
-        }
+        }
+        #endif
...
-            if self.merchant.name.lowercased().contains("gamestop") {
+            #if DEBUG
+            if self.merchant.name.lowercased().contains("gamestop") {
                 print("🎯 GAMESTOP DENOMINATIONS TYPE DEBUG:")
                 ...
-            }
+            }
+            #endif

Also applies to: 997-1041


1085-1095: Gate grabber setup debug prints behind DEBUG.

Consistent with logging policy.

-        print("DEBUG: grabberContainer is nil!")
+        #if DEBUG
+        print("DEBUG: grabberContainer is nil!")
+        #endif
...
-        print("DEBUG: Setting up pan gesture on grabber container")
+        #if DEBUG
+        print("DEBUG: Setting up pan gesture on grabber container")
+        #endif
...
-        print("DEBUG: Pan gesture added to grabber container")
+        #if DEBUG
+        print("DEBUG: Pan gesture added to grabber container")
+        #endif

640-656: US-only formatting noted — consider resilient fallback.

If digits don’t match US patterns, fall back to original string (you already do). Optional: label as “United States only” conditionally next to phone.

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

174-174: Trim trailing whitespace (SwiftLint).

Line 174 has trailing spaces flagged by SwiftLint.

-
+
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 973cbbd and 7becc33.

📒 Files selected for processing (5)
  • DashWallet/Sources/Models/Explore Dash/Services/CTXSpendEndpoint.swift (3 hunks)
  • DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (8 hunks)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (2)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendAPI.swift (2)
  • request (82-90)
  • request (92-113)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/CTXSpendUserAuthViewModel.swift (1)
  • logout (83-85)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7)
DashWallet/Sources/UI/Views/UIButton+Dash.swift (3)
  • action (60-70)
  • configuration (72-94)
  • image (21-29)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (2)
  • stopNetworkMonitoring (52-54)
  • startNetworkMonitoring (36-50)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)
  • callAction (84-99)
  • payAction (79-82)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (1)
  • configureGiftCardSection (22-71)
DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (1)
  • toSavingPercentages (78-80)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)
  • logout (90-108)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (3)
  • setupInfoButton (272-276)
  • configureHierarchy (232-238)
  • infoButtonAction (278-283)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)
  • configureHierarchy (203-226)
  • callAction (130-158)
  • websiteAction (176-180)
  • setupGrabberPanGesture (1084-1095)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)
  • configureHierarchy (337-493)
  • showMapAction (526-529)
  • showMap (109-120)
DashWallet/Sources/Categories/UIHostingController+DashWallet.swift (1)
  • setDetent (21-31)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendEndpoint.swift (2)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (2)
  • logout (90-108)
  • refreshToken (124-126)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/CTXSpendUserAuthViewModel.swift (1)
  • logout (83-85)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift

[Warning] 98-98: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift

[Warning] 174-174: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 81-81: Line should be 120 characters or less; currently it has 159 characters

(line_length)


[Warning] 82-82: Line should be 120 characters or less; currently it has 154 characters

(line_length)


[Warning] 84-84: Line should be 120 characters or less; currently it has 156 characters

(line_length)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 130-130: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 160-160: Limit vertical whitespace to a single empty line; currently 2

(vertical_whitespace)


[Warning] 205-205: Prefer object literals over image and color inits

(object_literal)


[Warning] 276-276: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 382-382: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 434-434: Line should be 120 characters or less; currently it has 181 characters

(line_length)


[Warning] 547-547: Line should be 120 characters or less; currently it has 125 characters

(line_length)


[Warning] 617-617: Line should be 120 characters or less; currently it has 126 characters

(line_length)


[Warning] 350-350: Prefer object literals over image and color inits

(object_literal)


[Warning] 481-481: Prefer object literals over image and color inits

(object_literal)


[Warning] 547-547: Prefer object literals over image and color inits

(object_literal)


[Warning] 814-814: Limit vertical whitespace to a single empty line; currently 3

(vertical_whitespace)


[Warning] 908-908: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 947-947: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 981-981: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 1049-1049: Line should be 120 characters or less; currently it has 129 characters

(line_length)


[Warning] 959-959: Prefer object literals over image and color inits

(object_literal)


[Warning] 1083-1083: Limit vertical whitespace to a single empty line; currently 2

(vertical_whitespace)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1128-1128: Unimplemented functions should be marked as unavailable

(unavailable_function)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 36-36: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 67-67: Line should be 120 characters or less; currently it has 121 characters

(line_length)


[Warning] 68-68: Line should be 120 characters or less; currently it has 124 characters

(line_length)


[Warning] 160-160: Line should be 120 characters or less; currently it has 136 characters

(line_length)


[Warning] 220-220: Line should be 120 characters or less; currently it has 132 characters

(line_length)


[Warning] 71-71: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 78-78: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 90-90: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 94-94: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 100-100: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 104-104: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 106-106: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 108-108: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 111-111: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 116-116: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 125-125: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 128-128: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 137-137: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 139-139: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 150-150: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 159-159: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 167-167: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 176-176: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 184-184: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 188-188: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 202-202: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 205-205: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 213-213: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 219-219: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 224-224: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 227-227: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 274-274: Force unwrapping should be avoided

(force_unwrapping)


[Warning] 270-270: Line should be 120 characters or less; currently it has 127 characters

(line_length)


[Warning] 271-271: Line should be 120 characters or less; currently it has 134 characters

(line_length)


[Warning] 266-266: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 272-272: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 319-319: Lines should not have trailing whitespace

(trailing_whitespace)

🔇 Additional comments (10)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendEndpoint.swift (2)

49-53: LGTM: new logout endpoint case added.

Enum surface looks consistent with the existing style.


71-80: Confirm backend contract for logout path.

Verify the server expects POST /logout (no body). If it’s a different verb or expects payload, adjust here accordingly.

DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)

91-107: Server-then-local logout with guaranteed cleanup.

Good: server attempt wrapped in Task; local token/device cleanup always runs on MainActor.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

66-77: Info button integration LGTM.
Uses SwiftUI sheet detent correctly; consistent with list screen.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)

248-258: Subtitle now excludes distance; address adds distance — LGTM.

Clear separation; refreshSubtitle updates both correctly.

Also applies to: 920-931


865-871: Button enablement logic LGTM.

Respects CTX enabled with fallback to local active + online + synced.


669-682: Ensure SDWebImage import/availability.

You call sd_setImage; verify SDWebImage is linked in this target.

Also applies to: 678-681

DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (3)

170-173: Good guard before purchase.

Blocking purchase when the CTX API marks the merchant unavailable is correct and aligns with the UX goal.


269-269: Correctly mapping CTX enabled to local state.

Assigning merchantInfo.enabled to isMerchantEnabled on successful fetch is spot on.


58-58: Initialize isMerchantEnabled from merchant.enabled with fallback to merchant.active
Seed the initial state in init(merchant:) instead of defaulting to true:

self.isMerchantEnabled = merchant.enabled ?? merchant.active

Verify that active exists on ExplorePointOfUse and represents the local availability flag.

Copy link

@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/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

176-181: Normalize website URL before opening.

Mirror the controller’s fix to handle missing schemes.

-        guard let website = merchant.website, let url = URL(string: website) else { return }
+        guard let website = merchant.website else { return }
+        let normalized = (website.hasPrefix("http://") || website.hasPrefix("https://")) ? website : "https://\(website)"
+        guard let url = URL(string: normalized) else { return }
         UIApplication.shared.open(url, options: [:], completionHandler: nil)
♻️ Duplicate comments (3)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (1)

283-285: Don’t disable on CTX fetch failure; preserve prior or local state.

Setting isMerchantEnabled = false conflates network errors with disablement; this regresses offline/temporary-outage UX.

         } catch {
             DSLogger.log("Failed to get merchant info: \(error)")
-            // Set merchant as disabled if we can't fetch info
-            isMerchantEnabled = false
         }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

266-278: Avoid force unwrapping contentViewTopConstraint in constraints array.

Use a local non-optional var; silences SwiftLint and removes a footgun.

-            contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            contentViewTopConstraint = topConstraint
...
-            constraint = [
-                contentViewTopConstraint!,
+            constraint = [
+                topConstraint,
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

130-158: Wrap verbose phone debug prints with DEBUG.

Prevents leaking debug logs in production.

-        print("DEBUG: Original phone: \(phone)")
+        #if DEBUG
+        print("DEBUG: Original phone: \(phone)")
+        #endif
...
-        print("DEBUG: Extracted digits: \(digits)")
+        #if DEBUG
+        print("DEBUG: Extracted digits: \(digits)")
+        #endif
...
-        print("DEBUG: URL string: \(urlString)")
+        #if DEBUG
+        print("DEBUG: URL string: \(urlString)")
+        #endif
...
-            print("DEBUG: Failed to create URL")
+            #if DEBUG
+            print("DEBUG: Failed to create URL")
+            #endif
...
-            print("DEBUG: Cannot open URL - phone not available")
+            #if DEBUG
+            print("DEBUG: Cannot open URL - phone not available")
+            #endif
...
-        print("DEBUG: Opening URL: \(url)")
-        UIApplication.shared.open(url, options: [:], completionHandler: { success in
-            print("DEBUG: URL open result: \(success)")
-        })
+        #if DEBUG
+        print("DEBUG: Opening URL: \(url)")
+        #endif
+        UIApplication.shared.open(url, options: [:], completionHandler: { success in
+            #if DEBUG
+            print("DEBUG: URL open result: \(success)")
+            #endif
+        })
🧹 Nitpick comments (13)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (2)

58-58: Consider tri-state availability (unknown/enabled/disabled).

Bool can’t represent “unknown”; an enum would make fallback logic explicit and avoid optimistic assumptions.


174-174: Trim trailing whitespace (SwiftLint).

Minor lint cleanup.

Also applies to: 299-299

DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (1)

67-77: Wrap long initializer for readability and lint.

Break parameters across lines; no behavior change.

-        init(merchantId: String, paymentMethod: PaymentMethod, type: `Type`, deeplink: String?, savingsBasisPoints: Int, denominationsType: String?, denominations: [Int] = [], redeemType: String?, enabled: Bool? = nil) {
+        init(
+            merchantId: String,
+            paymentMethod: PaymentMethod,
+            type: `Type`,
+            deeplink: String?,
+            savingsBasisPoints: Int,
+            denominationsType: String?,
+            denominations: [Int] = [],
+            redeemType: String?,
+            enabled: Bool? = nil
+        ) {
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)

101-108: Gate debug prints behind DEBUG.

Avoid noisy logs in release.

-        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #if DEBUG
+        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
         guard let contentViewTopConstraint = contentViewTopConstraint else { 
-            print("DEBUG: contentViewTopConstraint is nil!")
+            #if DEBUG
+            print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
             return 
         }
...
-            } completion: { [weak self] _ in
-                // Map button visibility update removed since button is removed
-                // self?.updateMapButtonVisibility()
-            }
+            } completion: { _ in }

Also applies to: 160-167


215-227: Use same position model as drag handler; avoid magic 100.

Compute kDefaultClosedMapPosition from screen height for consistency.

-        // Animate to show more map (closed position)
-        let kDefaultClosedMapPosition: CGFloat = 100.0
+        // Animate to show more map (closed position)
+        let screenHeight = view.frame.size.height
+        let kDefaultClosedMapPosition = screenHeight * 0.75

228-234: Derive visibility threshold instead of hard-coded 350.

Base on the same constants to adapt across devices.

-        // Show map button when content is mostly expanded (less map visible)
-        let shouldShow = contentViewTopConstraint.constant > 350
+        // Show when closer to closed position than open position
+        let h = view.frame.size.height
+        let open = h * 0.2
+        let closed = h * 0.75
+        let mid = (open + closed) / 2
+        let shouldShow = contentViewTopConstraint.constant > mid
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7)

988-1007: Gate CTX field debug prints with DEBUG (and prefer DSLogger).

Avoid noisy stdout; keep diagnostics behind compile flag.

-        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") {
-            print("🎯 MERCHANT DEBUG: \(self.merchant.name)")
-            ...
-            print("   ----------")
-        }
+        #if DEBUG
+        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") {
+            DSLogger.log("MERCHANT DEBUG: \(self.merchant.name) id=\(merchant.merchantId) denomType=\(merchant.denominationsType ?? "nil")")
+        }
+        #endif

1082-1093: Wrap grabber pan debug prints with DEBUG.

Same rationale as above.

-        guard let grabberContainer = grabberContainer else {
-            print("DEBUG: grabberContainer is nil!")
+        guard let grabberContainer = grabberContainer else {
+            #if DEBUG
+            print("DEBUG: grabberContainer is nil!")
+            #endif
             return
         }
-        print("DEBUG: Setting up pan gesture on grabber container")
+        #if DEBUG
+        print("DEBUG: Setting up pan gesture on grabber container")
+        #endif
...
-        print("DEBUG: Pan gesture added to grabber container")
+        #if DEBUG
+        print("DEBUG: Pan gesture added to grabber container")
+        #endif

29-30: Avoid IUO for grabberContainer.

Make it optional; code already guards against nil.

-    private var grabberContainer: UIView!
+    private var grabberContainer: UIView?

35-41: Align access control with enclosing type (SwiftLint lower_acl_than_parent).

These can be internal; no external API needed.

-    public var payWithDashHandler: (()->())?
-    public var sellDashHandler: (()->())?
-    public var dashSpendAuthHandler: (()->())?
-    public var buyGiftCardHandler: (()->())?
-    public var showAllLocationsActionBlock: (() -> ())?
-    public var infoButtonActionBlock: (() -> ())? // For parent view controller to handle info button tap
+    var payWithDashHandler: (() -> Void)?
+    var sellDashHandler: (() -> Void)?
+    var dashSpendAuthHandler: (() -> Void)?
+    var buyGiftCardHandler: (() -> Void)?
+    var showAllLocationsActionBlock: (() -> Void)?
+    var infoButtonActionBlock: (() -> Void)? // For parent view controller to handle info button tap

789-811: Country notice is hard-coded to US.

If expansion is planned, inject country/locale so text isn’t hard-coded.


594-600: Replace placeholder getLocationCount().

Wire actual count from data source or pass via initializer; prevents stale UI labels.

I can draft the parameter plumbing from the controller to this view; want me to?


81-86: Minor lint fixes: long lines and object literals.

Break long lines and prefer UIColor/UIImage literals to satisfy SwiftLint.

Also applies to: 617-621, 961-963

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7becc33 and 7c36679.

📒 Files selected for processing (4)
  • DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (3 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (8 hunks)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (6)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (2)
  • stopNetworkMonitoring (52-54)
  • startNetworkMonitoring (36-50)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)
  • callAction (84-99)
  • payAction (79-82)
  • websiteAction (173-187)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (1)
  • configureGiftCardSection (22-71)
DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (2)
  • toSavingPercentages (79-81)
  • isEnabled (88-90)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)
  • logout (90-108)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (2)
  • setupInfoButton (272-276)
  • infoButtonAction (278-283)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)
  • callAction (130-158)
  • websiteAction (176-180)
  • setupGrabberPanGesture (1082-1093)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (2)
  • showMapAction (526-529)
  • showMap (109-120)
DashWallet/Sources/Categories/UIHostingController+DashWallet.swift (1)
  • setDetent (21-31)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift

[Warning] 174-174: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 299-299: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 81-81: Line should be 120 characters or less; currently it has 159 characters

(line_length)


[Warning] 82-82: Line should be 120 characters or less; currently it has 154 characters

(line_length)


[Warning] 84-84: Line should be 120 characters or less; currently it has 156 characters

(line_length)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 130-130: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 160-160: Limit vertical whitespace to a single empty line; currently 2

(vertical_whitespace)


[Warning] 205-205: Prefer object literals over image and color inits

(object_literal)


[Warning] 276-276: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 382-382: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 434-434: Line should be 120 characters or less; currently it has 181 characters

(line_length)


[Warning] 547-547: Line should be 120 characters or less; currently it has 125 characters

(line_length)


[Warning] 617-617: Line should be 120 characters or less; currently it has 126 characters

(line_length)


[Warning] 350-350: Prefer object literals over image and color inits

(object_literal)


[Warning] 481-481: Prefer object literals over image and color inits

(object_literal)


[Warning] 547-547: Prefer object literals over image and color inits

(object_literal)


[Warning] 814-814: Limit vertical whitespace to a single empty line; currently 3

(vertical_whitespace)


[Warning] 906-906: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 945-945: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 979-979: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 1047-1047: Line should be 120 characters or less; currently it has 129 characters

(line_length)


[Warning] 957-957: Prefer object literals over image and color inits

(object_literal)


[Warning] 1081-1081: Limit vertical whitespace to a single empty line; currently 2

(vertical_whitespace)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1126-1126: Unimplemented functions should be marked as unavailable

(unavailable_function)

DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift

[Error] 67-67: Line should be 200 characters or less; currently it has 220 characters

(line_length)


[Warning] 66-66: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 87-87: Line should be 120 characters or less; currently it has 128 characters

(line_length)


[Warning] 86-86: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 36-36: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 67-67: Line should be 120 characters or less; currently it has 121 characters

(line_length)


[Warning] 68-68: Line should be 120 characters or less; currently it has 124 characters

(line_length)


[Warning] 160-160: Line should be 120 characters or less; currently it has 136 characters

(line_length)


[Warning] 220-220: Line should be 120 characters or less; currently it has 132 characters

(line_length)


[Warning] 71-71: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 78-78: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 90-90: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 94-94: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 100-100: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 104-104: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 106-106: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 108-108: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 111-111: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 116-116: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 125-125: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 128-128: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 137-137: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 139-139: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 150-150: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 159-159: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 167-167: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 176-176: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 184-184: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 188-188: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 202-202: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 205-205: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 213-213: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 219-219: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 224-224: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 227-227: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 274-274: Force unwrapping should be avoided

(force_unwrapping)


[Warning] 271-271: Line should be 120 characters or less; currently it has 134 characters

(line_length)


[Warning] 266-266: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 272-272: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 319-319: Lines should not have trailing whitespace

(trailing_whitespace)

🔇 Additional comments (3)
DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (1)

64-66: Enabled flag + fallback helper look good.

Optional enabled with isEnabled(fallbackActive:) matches the intended semantics.

Also applies to: 86-91

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

173-187: Hardened websiteAction LGTM.

Normalizing the URL fixes scheme-less inputs.


59-62: Info button integration LGTM.

Simple and self-contained; matches patterns used elsewhere.

Also applies to: 66-77

@bfoss765
Copy link
Contributor Author

bfoss765 commented Sep 5, 2025

Claude made changes for everything except the marketing version issues and this last comment (CR seems to find something new on each review...)

@HashEngineering
Copy link
Contributor

there is always something new.

Copy link

@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 (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

235-251: Avoid force-unwrapping latitude/longitude in showMapIfNeeded

Guard lat/lon to prevent a crash on malformed data.

-        mapView.show(merchants: [pointOfUse])
-        mapView.centerRadius = 5
-        mapView.initialCenterLocation = .init(latitude: pointOfUse.latitude!, longitude: pointOfUse.longitude!)
+        mapView.show(merchants: [pointOfUse])
+        mapView.centerRadius = 5
+        if let lat = pointOfUse.latitude, let lon = pointOfUse.longitude {
+            mapView.initialCenterLocation = .init(latitude: lat, longitude: lon)
+        }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

176-180: Normalize website URL and check canOpenURL before opening

Mirror controller logic and handle empty strings; prefer https default.

-    func websiteAction() {
-        guard let website = merchant.website, let url = URL(string: website) else { return }
-        UIApplication.shared.open(url, options: [:], completionHandler: nil)
-    }
+    func websiteAction() {
+        guard var website = merchant.website?.trimmingCharacters(in: .whitespacesAndNewlines),
+              !website.isEmpty else { return }
+        if !website.lowercased().hasPrefix("http") { website = "https://" + website }
+        guard let url = URL(string: website), UIApplication.shared.canOpenURL(url) else { return }
+        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+    }
♻️ Duplicate comments (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

173-186: Harden websiteAction URL normalization and empty-string handling

Trim, add scheme case-insensitively, and guard against empty strings; keep behavior consistent with List VC.

-    func websiteAction() {
-        guard let website = pointOfUse.website else { return }
-        
-        // Normalize URL by adding https scheme if missing
-        let normalizedWebsite: String
-        if website.hasPrefix("http://") || website.hasPrefix("https://") {
-            normalizedWebsite = website
-        } else {
-            normalizedWebsite = "https://" + website
-        }
-        
-        guard let url = URL(string: normalizedWebsite) else { return }
-        UIApplication.shared.open(url, options: [:], completionHandler: nil)
-    }
+    func websiteAction() {
+        guard var website = pointOfUse.website?.trimmingCharacters(in: .whitespacesAndNewlines),
+              !website.isEmpty else { return }
+        if !website.lowercased().hasPrefix("http") { website = "https://" + website }
+        guard let url = URL(string: website) else { return }
+        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+    }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

130-158: Wrap phone debug prints with #if DEBUG

These logs will ship to production as-is.

-    @objc func callAction() {
+    @objc func callAction() {
         guard let phone = merchant.phone, !phone.isEmpty else { return }
-        print("DEBUG: Original phone: \(phone)")
+        #if DEBUG
+        print("DEBUG: Original phone: \(phone)")
+        #endif
@@
-        print("DEBUG: Extracted digits: \(digits)")
+        #if DEBUG
+        print("DEBUG: Extracted digits: \(digits)")
+        #endif
@@
-        print("DEBUG: URL string: \(urlString)")
+        #if DEBUG
+        print("DEBUG: URL string: \(urlString)")
+        #endif
@@
-        guard let url = URL(string: urlString) else {
-            print("DEBUG: Failed to create URL")
+        guard let url = URL(string: urlString) else {
+            #if DEBUG
+            print("DEBUG: Failed to create URL")
+            #endif
             return
         }
@@
-        guard UIApplication.shared.canOpenURL(url) else {
-            print("DEBUG: Cannot open URL - phone not available")
+        guard UIApplication.shared.canOpenURL(url) else {
+            #if DEBUG
+            print("DEBUG: Cannot open URL - phone not available")
+            #endif
             return
         }
@@
-        print("DEBUG: Opening URL: \(url)")
-        UIApplication.shared.open(url, options: [:], completionHandler: { success in
-            print("DEBUG: URL open result: \(success)")
-        })
+        #if DEBUG
+        print("DEBUG: Opening URL: \(url)")
+        #endif
+        UIApplication.shared.open(url, options: [:], completionHandler: { success in
+            #if DEBUG
+            print("DEBUG: URL open result: \(success)")
+            #endif
+        })
     }
🧹 Nitpick comments (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)

189-233: Remove dead map button code (and property) or feature-flag it

showMapButton is not used (comment says it’s removed). Keeping code adds maintenance and confuses readers.

-    private var showMapButton: UIButton!
@@
-    private func setupMapButton() { ... }
-
-    @objc
-    private func showMapAction() { ... }
-
-    private func updateMapButtonVisibility() { ... }

If you intend to keep it, hide behind a compile-time flag and ensure constants are unified with handlePanGesture().

Also applies to: 35-37


498-512: Prefer model helper for enabled fallback

Reuse ExplorePointOfUse.Merchant.isEnabled(fallbackActive:) to avoid logic drift.

-            enabled: enabled ?? currentMerchant.enabled
+            enabled: enabled ?? currentMerchant.enabled

And downstream, use m.isEnabled(fallbackActive: self.active) where you check availability.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)

1056-1096: Gate grabber pan gesture debug prints behind DEBUG

Prevent noisy logs in production.

-        guard let grabberContainer = grabberContainer else {
-            print("DEBUG: grabberContainer is nil!")
+        guard let grabberContainer = grabberContainer else {
+            #if DEBUG
+            print("DEBUG: grabberContainer is nil!")
+            #endif
             return
         }
-        print("DEBUG: Setting up pan gesture on grabber container")
+        #if DEBUG
+        print("DEBUG: Setting up pan gesture on grabber container")
+        #endif
@@
-        print("DEBUG: Pan gesture added to grabber container")
+        #if DEBUG
+        print("DEBUG: Pan gesture added to grabber container")
+        #endif

856-869: Use model helper for availability and minimize duplication

Prefer m.isEnabled(fallbackActive:) to avoid repeating fallback logic and keep semantics in one place.

-        let isEnabled = m.enabled ?? merchant.active
+        let isEnabled = m.isEnabled(fallbackActive: merchant.active)

610-623: Trim website text before displaying

Minor polish: trim and strip scheme once, so “www.example.com/…” shows cleanly.

-        let displayText = website.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "http://", with: "")
+        let displayText = website
+            .trimmingCharacters(in: .whitespacesAndNewlines)
+            .replacingOccurrences(of: "https://", with: "")
+            .replacingOccurrences(of: "http://", with: "")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7c36679 and 6db576d.

📒 Files selected for processing (2)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (5)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (2)
  • stopNetworkMonitoring (52-54)
  • startNetworkMonitoring (36-50)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)
  • callAction (84-99)
  • payAction (79-82)
  • websiteAction (173-187)
DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (2)
  • toSavingPercentages (79-81)
  • isEnabled (88-90)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)
  • logout (90-108)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (3)
  • setupInfoButton (272-276)
  • configureHierarchy (232-238)
  • infoButtonAction (278-283)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (2)
  • configureHierarchy (203-226)
  • setupGrabberPanGesture (1085-1096)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)
  • configureHierarchy (337-493)
  • showMapAction (526-529)
  • showMap (109-120)
DashWallet/Sources/Categories/UIHostingController+DashWallet.swift (1)
  • setDetent (21-31)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 81-81: Line should be 120 characters or less; currently it has 159 characters

(line_length)


[Warning] 82-82: Line should be 120 characters or less; currently it has 154 characters

(line_length)


[Warning] 84-84: Line should be 120 characters or less; currently it has 156 characters

(line_length)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 130-130: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 160-160: Limit vertical whitespace to a single empty line; currently 2

(vertical_whitespace)


[Warning] 205-205: Prefer object literals over image and color inits

(object_literal)


[Warning] 276-276: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 382-382: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 434-434: Line should be 120 characters or less; currently it has 181 characters

(line_length)


[Warning] 547-547: Line should be 120 characters or less; currently it has 125 characters

(line_length)


[Warning] 617-617: Line should be 120 characters or less; currently it has 126 characters

(line_length)


[Warning] 350-350: Prefer object literals over image and color inits

(object_literal)


[Warning] 481-481: Prefer object literals over image and color inits

(object_literal)


[Warning] 547-547: Prefer object literals over image and color inits

(object_literal)


[Warning] 814-814: Limit vertical whitespace to a single empty line; currently 3

(vertical_whitespace)


[Warning] 906-906: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 945-945: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 979-979: Line should be 120 characters or less; currently it has 135 characters

(line_length)


[Warning] 996-996: Line should be 120 characters or less; currently it has 175 characters

(line_length)


[Warning] 1050-1050: Line should be 120 characters or less; currently it has 129 characters

(line_length)


[Warning] 957-957: Prefer object literals over image and color inits

(object_literal)


[Warning] 1084-1084: Limit vertical whitespace to a single empty line; currently 2

(vertical_whitespace)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1129-1129: Unimplemented functions should be marked as unavailable

(unavailable_function)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 36-36: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 67-67: Line should be 120 characters or less; currently it has 121 characters

(line_length)


[Warning] 68-68: Line should be 120 characters or less; currently it has 124 characters

(line_length)


[Warning] 160-160: Line should be 120 characters or less; currently it has 136 characters

(line_length)


[Warning] 220-220: Line should be 120 characters or less; currently it has 132 characters

(line_length)


[Warning] 71-71: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 78-78: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 90-90: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 94-94: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 100-100: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 104-104: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 106-106: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 108-108: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 111-111: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 116-116: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 125-125: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 128-128: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 137-137: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 139-139: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 150-150: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 159-159: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 167-167: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 176-176: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 184-184: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 188-188: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 202-202: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 205-205: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 213-213: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 219-219: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 224-224: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 227-227: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 274-274: Force unwrapping should be avoided

(force_unwrapping)


[Warning] 271-271: Line should be 120 characters or less; currently it has 134 characters

(line_length)


[Warning] 266-266: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 272-272: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 319-319: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 413-413: Line should be 120 characters or less; currently it has 122 characters

(line_length)


[Warning] 411-411: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 419-419: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 425-425: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 433-433: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 437-437: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 441-441: Lines should not have trailing whitespace

(trailing_whitespace)

Copy link

@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

♻️ Duplicate comments (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)

104-174: Unify sheet positions, avoid shadowing, and gate debug prints

  • Don’t shadow contentViewTopConstraint; use a local topConstraint.
  • Centralize positions via one helper to keep logic consistent across methods.
  • Gate debug prints with DEBUG.
-    private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
-        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
-        guard let contentViewTopConstraint = contentViewTopConstraint else { 
-            print("DEBUG: contentViewTopConstraint is nil!")
+    private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
+        #if DEBUG
+        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
+        guard let topConstraint = contentViewTopConstraint else {
+            #if DEBUG
+            print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
             return 
         }
         
         let translatedPoint: CGPoint = sender.translation(in: view)
-        let currentY = contentViewTopConstraint.constant
+        let currentY = topConstraint.constant
@@
-            // Constrain movement between closed position and maximum expanded position
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75 // Mostly closed (more map visible) 
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Default position higher by about an inch
-            let kDefaultOpenedMapPosition = screenHeight * 0.2 // Mostly open (less map visible, more content)
-            // Allow dragging between open position (top) and closed position (bottom)
-            let maxY = kDefaultClosedMapPosition // Don't allow dragging below closed position
-            let minY = kDefaultOpenedMapPosition // Don't allow dragging above open position
+            // Constrain movement between open and closed positions
+            let pos = mapPositions()
+            let maxY = pos.closed
+            let minY = pos.opened
             
-            contentViewTopConstraint.constant = max(minY, min(maxY, newY))
+            topConstraint.constant = max(minY, min(maxY, newY))
             sender.setTranslation(.zero, in: view)
@@
-            let finalCurrentY = contentViewTopConstraint.constant
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75 // Mostly closed (more map visible)
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Higher by about an inch
-            let kDefaultOpenedMapPosition = screenHeight * 0.2
+            let finalCurrentY = topConstraint.constant
+            let pos = mapPositions()
             
             var finalY: CGFloat
@@
-                finalY = kDefaultClosedMapPosition
+                finalY = pos.closed
@@
-                finalY = kDefaultOpenedMapPosition
+                finalY = pos.opened
@@
-                let midPoint1 = (kDefaultOpenedMapPosition + kDefaultBottomHalfPosition) / 2
-                let midPoint2 = (kDefaultBottomHalfPosition + kDefaultClosedMapPosition) / 2
+                let midPoint1 = (pos.opened + pos.half) / 2
+                let midPoint2 = (pos.half + pos.closed) / 2
                 
                 if finalCurrentY < midPoint1 {
-                    finalY = kDefaultOpenedMapPosition
+                    finalY = pos.opened
                 } else if finalCurrentY < midPoint2 {
-                    finalY = kDefaultBottomHalfPosition
+                    finalY = pos.half
                 } else {
-                    finalY = kDefaultClosedMapPosition
+                    finalY = pos.closed
                 }
             }
             
             UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseOut) {
-                contentViewTopConstraint.constant = finalY
+                topConstraint.constant = finalY
                 self.view.layoutIfNeeded()
             } completion: { [weak self] _ in
                 // Map button visibility update removed since button is removed
                 // self?.updateMapButtonVisibility()
             }

Add once in the type:

private func mapPositions() -> (opened: CGFloat, half: CGFloat, closed: CGFloat) {
    let h = view.bounds.height
    return (opened: h * 0.2, half: h * 0.5 - 72, closed: h * 0.75)
}

178-189: Harden website URL normalization (trim + case-insensitive scheme)

-        guard let website = pointOfUse.website else { return }
-        
-        // Normalize URL by adding https scheme if missing
-        let normalizedWebsite: String
-        if website.hasPrefix("http://") || website.hasPrefix("https://") {
-            normalizedWebsite = website
-        } else {
-            normalizedWebsite = "https://" + website
-        }
-        
-        guard let url = URL(string: normalizedWebsite) else { return }
+        guard var website = pointOfUse.website?.trimmingCharacters(in: .whitespacesAndNewlines),
+              !website.isEmpty else { return }
+        if URL(string: website)?.scheme == nil {
+            website = "https://\(website)"
+        }
+        guard let url = URL(string: website) else { return }

269-277: Avoid force unwrap and reuse unified position helper

-            // Create the top constraint and store reference to it
-            // Start higher than bottom half by about an inch (72 points)
-            let screenHeight = UIScreen.main.bounds.height
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Higher by about an inch
-            contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            // Create the top constraint and store reference to it
+            let pos = mapPositions()
+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: pos.half)
+            contentViewTopConstraint = topConstraint
@@
-            constraint = [
-                contentViewTopConstraint!,
+            constraint = [
+                topConstraint,

419-427: Gate CTX merchant debug prints behind DEBUG

Avoid leaking merchant details in production logs.

-                // Debug logging for CTX merchant info
-                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
-                    print("🎯 CTX MERCHANT INFO DEBUG: \(pointOfUse.name)")
-                    print("   CTX enabled: \(merchantInfo.enabled)")
-                    print("   Local active: \(pointOfUse.active)")
-                    print("   Will update view with enabled: \(merchantInfo.enabled)")
-                }
+                #if DEBUG
+                // Debug logging for CTX merchant info
+                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
+                    print("🎯 CTX MERCHANT INFO DEBUG: \(pointOfUse.name)")
+                    print("   CTX enabled: \(merchantInfo.enabled)")
+                    print("   Local active: \(pointOfUse.active)")
+                    print("   Will update view with enabled: \(merchantInfo.enabled)")
+                }
+                #endif
DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1)

48-50: Logout constant is not a real logout; add token revoke endpoint and use it.

Keep dashboard URL for post-logout navigation, but add a revoke URL constant and call it before navigating. This was raised earlier; still applicable.

Add alongside logout (and declare in the header):

+ (NSString *)revokeTokenURLString {
    return @"https://api.uphold.com/oauth2/revoke";
}

Please confirm sandbox parity (DWUpholdSandboxConstants.{h,m}) and that the logout flow actually calls revoke, clears local creds, then opens the dashboard.

🧹 Nitpick comments (11)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (5)

26-26: Avoid IUO for distanceLabel (or remove it entirely).

Using UILabel! risks a crash if update(with:) runs before configureHierarchy(). Since the label is never shown (kept hidden) and subLabel now owns distance, prefer removing it.

Apply this diff to delete the unused property:

-    private var distanceLabel: UILabel!

33-42: DRY up distance formatting; consider a shared helper.

This block duplicates distance logic used in AtmItemCell. Extract to a common helper (e.g., ExploreDash.formattedDistance(from:to:)) to keep rounding/units consistent and simplify testing.

Do we intentionally hide subLabel when location is unavailable for merchants (while ATM shows a source fallback)? Confirm UX parity or divergence.


46-46: Trim trailing whitespace.

Minor SwiftLint warning; remove trailing spaces on this line.


47-48: Remove redundant hiding of an unused label.

Since distance is shown via subLabel and distanceLabel is never displayed, this hide is unnecessary.

Apply this diff:

-        // Hide separate distance label since we're using subLabel
-        distanceLabel.isHidden = true

67-75: Don't add unused views to the stack.

distanceLabel is created, configured, and added to mainStackView but never used. This adds view hierarchy overhead and noise. Remove it; use subLabel exclusively.

Apply this diff:

-        // Distance label (separate from merchant name)
-        distanceLabel = UILabel()
-        distanceLabel.translatesAutoresizingMaskIntoConstraints = false
-        distanceLabel.font = .systemFont(ofSize: 12, weight: .regular)
-        distanceLabel.textColor = .dw_tertiaryText()
-        distanceLabel.isHidden = true
-        distanceLabel.textAlignment = .right
-        mainStackView.addArrangedSubview(distanceLabel)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (5)

52-56: Clamp bottom inset to non-negative

Avoid negative insets when detailsView > mapView.

-            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: mapView.frame.height - detailsView.frame.height - 10,
-                                               right: 0)
+            let bottom = max(mapView.frame.height - detailsView.frame.height - 10, 0)
+            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0)

89-101: Add tel: fallback if telprompt: can’t open

Some devices/contexts don’t support telprompt:. Fallback to tel: improves reliability.

-        let urlString = "telprompt:\(digits)"
-        guard let url = URL(string: urlString) else { return }
-        
-        // Check if device can open the URL
-        guard UIApplication.shared.canOpenURL(url) else { return }
-
-        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+        let prompt = URL(string: "telprompt:\(digits)")
+        let tel = URL(string: "tel:\(digits)")
+        if let url = prompt, UIApplication.shared.canOpenURL(url) {
+            UIApplication.shared.open(url, options: [:], completionHandler: nil)
+        } else if let url = tel, UIApplication.shared.canOpenURL(url) {
+            UIApplication.shared.open(url, options: [:], completionHandler: nil)
+        }

192-215: Remove unused Map button setup block

Feature is removed; keeping this risks future misuse.

-    private func setupMapButton() {
-        showMapButton = UIButton(type: .custom)
-        showMapButton.translatesAutoresizingMaskIntoConstraints = false
-        showMapButton.isHidden = true // Initially hidden
-        showMapButton.tintColor = .white
-        showMapButton.imageEdgeInsets = .init(top: 0, left: -10, bottom: 0, right: 0)
-        showMapButton.addTarget(self, action: #selector(showMapAction), for: .touchUpInside)
-        showMapButton.setImage(UIImage(systemName: "map.fill"), for: .normal)
-        showMapButton.setTitle(NSLocalizedString("Map", comment: ""), for: .normal)
-        showMapButton.layer.masksToBounds = true
-        showMapButton.layer.cornerRadius = 20
-        showMapButton.layer.backgroundColor = UIColor.black.cgColor
-        contentView.addSubview(showMapButton)
-        
-        let showMapButtonWidth: CGFloat = 92
-        let showMapButtonHeight: CGFloat = 40
-        
-        NSLayoutConstraint.activate([
-            showMapButton.widthAnchor.constraint(equalToConstant: showMapButtonWidth),
-            showMapButton.heightAnchor.constraint(equalToConstant: showMapButtonHeight),
-            showMapButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
-            showMapButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -15),
-        ])
-    }

217-229: Drop orphaned showMapAction (no caller) or align with unified positions

Since the Map button is gone, remove this method to avoid confusion.

-    @objc
-    private func showMapAction() {
-        // Animate to show more map (closed position)
-        let kDefaultClosedMapPosition: CGFloat = 100.0
-        guard let contentViewTopConstraint = contentViewTopConstraint else { return }
-        
-        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseOut) {
-            contentViewTopConstraint.constant = kDefaultClosedMapPosition
-            self.view.layoutIfNeeded()
-        }
-        
-        updateMapButtonVisibility()
-    }

231-236: Remove unused updateMapButtonVisibility (and crash-prone IUO access)

-    private func updateMapButtonVisibility() {
-        guard let contentViewTopConstraint = contentViewTopConstraint else { return }
-        // Show map button when content is mostly expanded (less map visible)
-        let shouldShow = contentViewTopConstraint.constant > 350
-        showMapButton.isHidden = !shouldShow
-    }
DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1)

24-26: Avoid duplicating the client ID in the authorize URL; construct it from the single source.

Keeps ID rotatable, and prevents divergence. Also preserves the state placeholder for a second formatting pass.

-    return @"https://wallet.uphold.com/authorize/c184650d0cb44e73d8e5cb2021753a721c41f74a?scope=accounts:read%%20cards:read%%20cards:write%%20transactions:deposit%%20transactions:read%%20transactions:transfer:application%%20transactions:transfer:others%%20transactions:transfer:self%%20transactions:withdraw%%20transactions:commit:otp%%20user:read&state=%@";
+    return [NSString stringWithFormat:
+            @"https://wallet.uphold.com/authorize/%@?scope=accounts:read%%20cards:read%%20cards:write%%20transactions:deposit%%20transactions:read%%20transactions:transfer:application%%20transactions:transfer:others%%20transactions:transfer:self%%20transactions:withdraw%%20transactions:commit:otp%%20user:read&state=%%@",
+            [self clientID]];

Nice-to-have: build this with NSURLComponents and URL-encode scopes instead of pre-encoding with %20.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6db576d and 339a09f.

📒 Files selected for processing (3)
  • DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (8 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (4)
  • mapView (239-244)
  • mapView (246-256)
  • mapView (272-278)
  • configureHierarchy (203-233)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (2)
  • configureHierarchy (337-493)
  • showMap (109-120)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)
  • configureHierarchy (203-226)
  • payAction (182-196)
  • callAction (130-158)
  • setupGrabberPanGesture (1085-1096)
DashWallet/Sources/Categories/UIHostingController+DashWallet.swift (1)
  • setDetent (21-31)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift (1)
  • update (41-49)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (1)
  • update (28-48)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 36-36: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 54-54: Line should be 120 characters or less; currently it has 126 characters

(line_length)


[Warning] 70-70: Line should be 120 characters or less; currently it has 121 characters

(line_length)


[Warning] 71-71: Line should be 120 characters or less; currently it has 124 characters

(line_length)


[Warning] 163-163: Line should be 120 characters or less; currently it has 136 characters

(line_length)


[Warning] 223-223: Line should be 120 characters or less; currently it has 132 characters

(line_length)


[Warning] 74-74: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 81-81: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 93-93: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 97-97: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 103-103: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 107-107: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 109-109: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 111-111: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 114-114: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 119-119: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 128-128: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 131-131: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 140-140: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 142-142: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 153-153: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 162-162: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 170-170: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 179-179: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 187-187: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 191-191: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 205-205: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 208-208: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 216-216: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 222-222: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 227-227: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 230-230: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 55-55: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 277-277: Force unwrapping should be avoided

(force_unwrapping)


[Warning] 274-274: Line should be 120 characters or less; currently it has 134 characters

(line_length)


[Warning] 269-269: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 275-275: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 299-299: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 327-327: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 421-421: Line should be 120 characters or less; currently it has 122 characters

(line_length)


[Warning] 419-419: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 427-427: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 433-433: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 441-441: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 445-445: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 449-449: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift

[Warning] 26-26: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 46-46: Lines should not have trailing whitespace

(trailing_whitespace)

🔇 Additional comments (7)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (6)

62-63: Info button integration looks good


69-81: Detented SwiftUI info sheet: OK

Clean, self-contained, consistent with setDetent helper.


295-335: Nice nil-safety for detailsView creation and targeted pan gesture

Good guard + targeted gesture on grabber.


434-437: MainActor UI refresh: good


442-448: Recreate details view after merchant update: good


507-520: updatingMerchant merges enabled safely and preserves fields

Looks correct; denom parsing with compactMap is safe.

DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1)

40-46: LGTM on wallet deep-links for card add and transaction view.

URLs look consistent and readable.

Copy link

@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

♻️ Duplicate comments (6)
DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1)

48-50: Logout URL doesn’t revoke tokens — track via follow-up

This navigates to a dashboard page and won’t invalidate OAuth tokens. Prior comment suggested adding a revokeTokenURLString and calling the revoke endpoint before navigation; agreed to handle in a separate story. Please add a TODO with the ticket ID and ensure sandbox parity when you tackle it.

Verify header parity and presence of a sandbox constant:

#!/bin/bash
fd DWUphold*Constants.h -t f | xargs -I{} rg -n 'logoutURLString|revokeTokenURLString' {}
fd DWUpholdSandboxConstants.m -t f | xargs -I{} rg -n 'logoutURLString|revoke' {}
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (5)

104-174: Unify sheet positions, avoid shadowing, animate during drag, and gate debug prints

  • Centralize opened/half/closed constants to a helper used everywhere.
  • Don’t shadow contentViewTopConstraint; use a local alias with a different name.
  • Call layoutIfNeeded() in .changed for smooth dragging.
  • Wrap debug prints in DEBUG.
-    private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
-        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
-        guard let contentViewTopConstraint = contentViewTopConstraint else { 
-            print("DEBUG: contentViewTopConstraint is nil!")
-            return 
-        }
+    private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
+        #if DEBUG
+        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
+        guard let topConstraint = contentViewTopConstraint else {
+            #if DEBUG
+            print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
+            return
+        }
         
         let translatedPoint: CGPoint = sender.translation(in: view)
-        let currentY = contentViewTopConstraint.constant
+        let currentY = topConstraint.constant
         
         switch sender.state {
         case .changed:
             // Only handle vertical movement and constrain within bounds
             let newY = currentY + translatedPoint.y
             
             // Constrain movement between closed position and maximum expanded position
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75 // Mostly closed (more map visible) 
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Default position higher by about an inch
-            let kDefaultOpenedMapPosition = screenHeight * 0.2 // Mostly open (less map visible, more content)
+            let pos = mapPositions()
             // Allow dragging between open position (top) and closed position (bottom)
-            let maxY = kDefaultClosedMapPosition // Don't allow dragging below closed position
-            let minY = kDefaultOpenedMapPosition // Don't allow dragging above open position
+            let maxY = pos.closed   // Don't allow dragging below closed position
+            let minY = pos.opened   // Don't allow dragging above open position
             
-            contentViewTopConstraint.constant = max(minY, min(maxY, newY))
+            topConstraint.constant = max(minY, min(maxY, newY))
             sender.setTranslation(.zero, in: view)
+            self.view.layoutIfNeeded()
             
         case .ended:
             let velocityInView = sender.velocity(in: view)
             let velocityY: CGFloat = velocityInView.y
-            let finalCurrentY = contentViewTopConstraint.constant
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75 // Mostly closed (more map visible)
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Higher by about an inch
-            let kDefaultOpenedMapPosition = screenHeight * 0.2
+            let finalCurrentY = topConstraint.constant
+            let pos = mapPositions()
             
             var finalY: CGFloat
             
             if velocityY > 300 {
                 // Fast downward swipe - snap to closed position (more map visible)
-                finalY = kDefaultClosedMapPosition
+                finalY = pos.closed
             } else if velocityY < -300 {
                 // Fast upward swipe - snap to open position (less map visible)
-                finalY = kDefaultOpenedMapPosition
+                finalY = pos.opened
             } else {
                 // No strong velocity, snap to nearest position based on current position
-                let midPoint1 = (kDefaultOpenedMapPosition + kDefaultBottomHalfPosition) / 2
-                let midPoint2 = (kDefaultBottomHalfPosition + kDefaultClosedMapPosition) / 2
+                let midPoint1 = (pos.opened + pos.half) / 2
+                let midPoint2 = (pos.half + pos.closed) / 2
                 
                 if finalCurrentY < midPoint1 {
-                    finalY = kDefaultOpenedMapPosition
+                    finalY = pos.opened
                 } else if finalCurrentY < midPoint2 {
-                    finalY = kDefaultBottomHalfPosition
+                    finalY = pos.half
                 } else {
-                    finalY = kDefaultClosedMapPosition
+                    finalY = pos.closed
                 }
             }
             
             UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseOut) {
-                contentViewTopConstraint.constant = finalY
+                topConstraint.constant = finalY
                 self.view.layoutIfNeeded()
             } completion: { [weak self] _ in
                 // Map button visibility update removed since button is removed
                 // self?.updateMapButtonVisibility()
             }

Add this helper in the type (outside the range above):

// Consistent map “sheet” positions
private func mapPositions() -> (opened: CGFloat, half: CGFloat, closed: CGFloat) {
    let h = view.bounds.height
    return (opened: h * 0.2, half: h * 0.5 - 72, closed: h * 0.75)
}

176-189: Harden websiteAction URL handling (trim, case-insensitive, canOpenURL, safer parsing)

Implements prior suggestion but still misses trimming and robust parsing.

-        guard let website = pointOfUse.website else { return }
-        
-        // Normalize URL by adding https scheme if missing
-        let normalizedWebsite: String
-        if website.hasPrefix("http://") || website.hasPrefix("https://") {
-            normalizedWebsite = website
-        } else {
-            normalizedWebsite = "https://" + website
-        }
-        
-        guard let url = URL(string: normalizedWebsite) else { return }
-        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+        guard var website = pointOfUse.website?.trimmingCharacters(in: .whitespacesAndNewlines),
+              !website.isEmpty else { return }
+        if !website.lowercased().hasPrefix("http") { website = "https://\(website)" }
+        guard let comps = URLComponents(string: website), let url = comps.url else { return }
+        guard UIApplication.shared.canOpenURL(url) else { return }
+        UIApplication.shared.open(url, options: [:], completionHandler: nil)

218-229: Use unified positions and avoid magic 100.0 in showMapAction

Mirror the same sheet constants used elsewhere; also avoid shadowing.

-        // Animate to show more map (closed position)
-        let kDefaultClosedMapPosition: CGFloat = 100.0
-        guard let contentViewTopConstraint = contentViewTopConstraint else { return }
+        // Animate to show more map (closed position)
+        let pos = mapPositions()
+        guard let topConstraint = contentViewTopConstraint else { return }
         
         UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseOut) {
-            contentViewTopConstraint.constant = kDefaultClosedMapPosition
+            topConstraint.constant = pos.closed
             self.view.layoutIfNeeded()
         }
         
         updateMapButtonVisibility()

415-427: Gate verbose CTX merchant debug logs behind DEBUG

Prevents leaking merchant state to release logs.

-                // Debug logging for CTX merchant info
-                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
+                #if DEBUG
+                // Debug logging for CTX merchant info
+                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
                     print("🎯 CTX MERCHANT INFO DEBUG: \(pointOfUse.name)")
                     print("   CTX enabled: \(merchantInfo.enabled)")
                     print("   Local active: \(pointOfUse.active)")
                     print("   Will update view with enabled: \(merchantInfo.enabled)")
                 }
+                #endif

270-277: Avoid force unwrap on contentViewTopConstraint to prevent crash

Create the constraint, assign it to the property, and use the local reference.

-            let screenHeight = UIScreen.main.bounds.height
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Higher by about an inch
-            contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            let screenHeight = UIScreen.main.bounds.height
+            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Higher by about an inch
+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            contentViewTopConstraint = topConstraint
@@
-            constraint = [
-                contentViewTopConstraint!,
+            constraint = [
+                topConstraint,
🧹 Nitpick comments (11)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (4)

26-26: Remove unused distanceLabel (and avoid IUO).

The label is never shown and only increases view hierarchy/complexity. Drop it to eliminate the implicitly unwrapped optional.

-    private var distanceLabel: UILabel!

33-45: Show a fallback subtitle when distance is unavailable (align with ATM cell).

Keep the subtitle useful instead of hiding it; mirror AtmItemCell’s behavior by showing the source if available.

-        } else {
-            subLabel.isHidden = true
-        }
+        } else if let source = pointOfUse.source {
+            subLabel.isHidden = false
+            subLabel.text = source
+        } else {
+            subLabel.isHidden = true
+        }

46-49: Remove redundant hide and trailing whitespace.

Label is already configured hidden (and slated for removal); this block is unnecessary. Also fixes SwiftLint trailing whitespace at Line 46.

-
-        // Hide separate distance label since we're using subLabel
-        distanceLabel.isHidden = true

67-75: Delete unused distanceLabel setup.

It’s not used; removing avoids extra arrangedSubview and layout work.

-        // Distance label (separate from merchant name)
-        distanceLabel = UILabel()
-        distanceLabel.translatesAutoresizingMaskIntoConstraints = false
-        distanceLabel.font = .systemFont(ofSize: 12, weight: .regular)
-        distanceLabel.textColor = .dw_tertiaryText()
-        distanceLabel.isHidden = true
-        distanceLabel.textAlignment = .right
-        mainStackView.addArrangedSubview(distanceLabel)
DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1)

44-46: Clarify that this is a web UI deep link, not an API endpoint

Add a brief doc comment to prevent misuse.

-+ (NSString *)transactionURLFormat {
+// Web UI deep link for viewing a transaction on Uphold (not an API endpoint).
++ (NSString *)transactionURLFormat {
     return @"https://wallet.uphold.com/reserve/transactions/%@";
 }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (6)

52-56: Clamp negative map inset to avoid odd layout on small screens

If detailsView is taller than mapView, bottom inset becomes negative. Clamp to 0.

-            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: mapView.frame.height - detailsView.frame.height - 10,
-                                               right: 0)
+            let bottomInset = max(0, mapView.frame.height - detailsView.frame.height - 10)
+            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)

69-73: Prefer template image + tint over pre-tinted original for adaptability

Let the bar button inherit nav bar tint and dynamic colors.

-        let infoImage = UIImage(systemName: "info.circle")?.withRenderingMode(.alwaysOriginal).withTintColor(.systemBlue)
-        let infoButton = UIBarButtonItem(image: infoImage, style: .plain, target: self, action: #selector(infoButtonAction))
+        let infoImage = UIImage(systemName: "info.circle")
+        let infoButton = UIBarButtonItem(image: infoImage, style: .plain, target: self, action: #selector(infoButtonAction))
+        infoButton.tintColor = .systemBlue
         navigationItem.rightBarButtonItem = infoButton

192-215: Dead code: map button setup appears unused

setupMapButton() isn’t called, and showMapButton stays hidden. Remove these bits or wire them back in; otherwise they add maintenance cost.


231-236: Derive visibility threshold from unified sheet positions

If keeping the button, base the threshold on mapPositions() instead of a hard-coded 350.

-        // Show map button when content is mostly expanded (less map visible)
-        let shouldShow = contentViewTopConstraint.constant > 350
+        // Show when content is mostly expanded (near opened/half)
+        let pos = mapPositions()
+        let shouldShow = contentViewTopConstraint.constant <= pos.half
         showMapButton.isHidden = !shouldShow

265-269: Conflicting clipping flags

clipsToBounds = false followed by layer.masksToBounds = true is contradictory. Set a single source of truth.

-            contentView.clipsToBounds = false
-            contentView.layer.masksToBounds = true
+            contentView.clipsToBounds = true

416-440: Handle CTX merchant fetch errors to avoid unhandled task failures

Wrap awaits in do/catch and surface a non-blocking path on failure.

-        Task {
-            if try await tryRefreshCtxToken(), let merchantId = pointOfUse.merchant?.merchantId {
-                let merchantInfo = try await CTXSpendService.shared.getMerchant(merchantId: merchantId)
+        Task {
+            do {
+                if try await tryRefreshCtxToken(), let merchantId = pointOfUse.merchant?.merchantId {
+                    let merchantInfo = try await CTXSpendService.shared.getMerchant(merchantId: merchantId)
@@
-                pointOfUse = pointOfUse.updatingMerchant(
+                    pointOfUse = pointOfUse.updatingMerchant(
@@
-                await MainActor.run {
-                    refreshDetailsViewWithUpdatedMerchant()
-                }
-            }
+                    await MainActor.run { refreshDetailsViewWithUpdatedMerchant() }
+                }
+            } catch {
+                #if DEBUG
+                print("CTX merchant fetch failed: \(error)")
+                #endif
+            }
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6db576d and 339a09f.

📒 Files selected for processing (3)
  • DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (8 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (4)
  • mapView (239-244)
  • mapView (246-256)
  • mapView (272-278)
  • configureHierarchy (203-233)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)
  • configureHierarchy (337-493)
  • showMapAction (526-529)
  • showMap (109-120)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)
  • configureHierarchy (203-226)
  • callAction (130-158)
  • websiteAction (176-180)
  • setupGrabberPanGesture (1085-1096)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift (1)
  • update (41-49)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (1)
  • update (28-48)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 36-36: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 54-54: Line should be 120 characters or less; currently it has 126 characters

(line_length)


[Warning] 70-70: Line should be 120 characters or less; currently it has 121 characters

(line_length)


[Warning] 71-71: Line should be 120 characters or less; currently it has 124 characters

(line_length)


[Warning] 163-163: Line should be 120 characters or less; currently it has 136 characters

(line_length)


[Warning] 223-223: Line should be 120 characters or less; currently it has 132 characters

(line_length)


[Warning] 74-74: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 81-81: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 93-93: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 97-97: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 103-103: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 107-107: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 109-109: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 111-111: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 114-114: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 119-119: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 128-128: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 131-131: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 140-140: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 142-142: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 153-153: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 162-162: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 170-170: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 179-179: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 187-187: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 191-191: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 205-205: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 208-208: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 216-216: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 222-222: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 227-227: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 230-230: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 55-55: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 277-277: Force unwrapping should be avoided

(force_unwrapping)


[Warning] 274-274: Line should be 120 characters or less; currently it has 134 characters

(line_length)


[Warning] 269-269: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 275-275: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 299-299: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 327-327: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 421-421: Line should be 120 characters or less; currently it has 122 characters

(line_length)


[Warning] 419-419: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 427-427: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 433-433: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 441-441: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 445-445: Lines should not have trailing whitespace

(trailing_whitespace)


[Warning] 449-449: Lines should not have trailing whitespace

(trailing_whitespace)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift

[Warning] 26-26: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 46-46: Lines should not have trailing whitespace

(trailing_whitespace)

🔇 Additional comments (5)
DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (2)

32-34: Client ID constant: OK

Shipping client IDs in public apps is expected. No change needed.


40-42: No changes needed for buyCardURLFormat
The card.identifier property is a NSString * on DWUpholdCardObject and is passed directly into the %@ placeholder, matching the API’s expected card UUID format.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)

89-101: Phone dialing flow is now robust

Nice digit sanitization and canOpenURL check; reduces failures on malformed input.


295-301: Good safety improvements around details view creation and pan wiring

Guarding nil from detailsView(for:) removes an IUO crash class, and scoping the pan gesture to the grabber is appropriate.

Also applies to: 321-326, 329-334


428-433: Model plumbing for denominations/enabled looks correct

Passing enabled through updatingMerchant and preserving existing fields is clean.

Also applies to: 507-520

@Syn-McJ
Copy link
Member

Syn-McJ commented Sep 6, 2025

This PR has a lot of changes to the old Merchant Details screen (not sure why) - this will cause a huge amount of conflicts with the feature/piggycards branch, where the new UI is implemented.

If the intent is to make modifications to the live DashDirect feature, I'd recommend keeping the changes as small as possible.
Alternatively, you can merge feature/piggycards into master and hide the extra functionality until PiggyCards are ready.

Also, Uphold secrets are exposed.

@bfoss765 bfoss765 force-pushed the improvement/merchant-country-info branch from 339a09f to 614a46a Compare September 8, 2025 19:44
Copy link

@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

♻️ Duplicate comments (11)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (3)

170-174: Remove duplicate merchant availability check.

This check is redundant as purchaseGiftCardAPI() already performs the same validation at line 295-298.

Remove this duplicate guard to avoid maintenance burden:

-        // Check if merchant is enabled before attempting purchase
-        guard isMerchantEnabled else {
-            throw CTXSpendError.merchantUnavailable
-        }
-        
         let response = try await purchaseGiftCardAPI()

283-285: Preserve merchant availability state on network failures.

Setting isMerchantEnabled = false on any fetch failure will incorrectly disable the merchant during transient network issues, preventing offline purchases or retries.

Don't conflate network failures with merchant availability:

         } catch {
             DSLogger.log("Failed to get merchant info: \(error)")
-            // Set merchant as disabled if we can't fetch info
-            isMerchantEnabled = false
+            // Keep the last known state or fall back to local active status
+            // Network failures shouldn't disable the merchant
         }

Consider showing a retry option or network error banner instead of blocking purchases entirely.


294-299: Use consistent error type for merchant unavailable.

The error type should be CTXSpendError.merchantUnavailable to match the check at line 172 and provide consistent error handling.

         // Additional server-side check for merchant availability
         guard isMerchantEnabled else {
             DSLogger.log("Purchase blocked: merchant disabled")
-            throw CTXSpendError.paymentProcessingError("Merchant temporarily unavailable")
+            throw CTXSpendError.merchantUnavailable
         }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)

594-600: Replace hardcoded location count with dynamic value.

The getLocationCount() method returns a hardcoded value of 11, which won't reflect the actual number of merchant locations.

Add a property to receive the actual location count from the parent view controller:

+    var locationCount: Int = 1
+    
     private func getLocationCount() -> Int {
-        // TODO: This should be injected from the parent view controller or data source
-        // For now, return a placeholder value + 1 to include the current location.
-        // The actual implementation should come from the data source that knows how many locations this merchant has
-        return 11 // 10 + 1 for current location, as requested
+        return locationCount
     }

Then update the parent view controller to set this value when initializing the view.


996-1010: Gate merchant debug logging behind DEBUG flag.

These debug prints include sensitive merchant data like merchantId and should not appear in production logs.

         // Comprehensive debug logging for merchant fields investigation
+        #if DEBUG
         if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") || self.merchant.name.lowercased().contains("buffalo") {
             print("🎯 MERCHANT DEBUG: \(self.merchant.name)")
             print("   merchantId: '\(merchant.merchantId)'")
             print("   paymentMethod: '\(merchant.paymentMethod)'")
             print("   type: '\(merchant.type)'")
             print("   deeplink: '\(merchant.deeplink ?? "nil")'")
             print("   savingsBasisPoints: '\(merchant.savingsBasisPoints)'")
             print("   denominationsType: '\(merchant.denominationsType ?? "nil")'")
             print("   denominations: '\(merchant.denominations)'")
             print("   redeemType: '\(merchant.redeemType ?? "nil")'")
             print("   enabled: '\(merchant.enabled?.description ?? "nil")'")
             print("   self.merchant.active: '\(self.merchant.active)'")
             print("   isEnabled: '\(isEnabled)'")
             print("   ----------")
         }
+        #endif

130-158: Remove debug print statements from production code.

These verbose debug prints will appear in production logs and should be gated behind DEBUG compilation flag.

     @objc func callAction() {
         guard let phone = merchant.phone, !phone.isEmpty else { return }
+        #if DEBUG
         print("DEBUG: Original phone: \(phone)")
+        #endif
 
         // Extract only digits for phone call
         let digits = phone.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
         guard !digits.isEmpty else { return }
+        #if DEBUG
         print("DEBUG: Extracted digits: \(digits)")
+        #endif
 
         // Use telprompt: to directly open phone app (tel: shows options)
         let urlString = "telprompt:\(digits)"
+        #if DEBUG
         print("DEBUG: URL string: \(urlString)")
+        #endif
 
         guard let url = URL(string: urlString) else {
+            #if DEBUG
             print("DEBUG: Failed to create URL")
+            #endif
             return
         }
 
         // Check if device can open the URL
         guard UIApplication.shared.canOpenURL(url) else {
+            #if DEBUG
             print("DEBUG: Cannot open URL - phone not available")
+            #endif
             return
         }
 
+        #if DEBUG
         print("DEBUG: Opening URL: \(url)")
+        #endif
         UIApplication.shared.open(url, options: [:], completionHandler: { success in
+            #if DEBUG
             print("DEBUG: URL open result: \(success)")
+            #endif
         })
     }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (5)

218-227: Fix inconsistent map position constant.

The showMapAction method uses a hardcoded value of 100.0 which doesn't match the sheet position calculations elsewhere.

     @objc
     private func showMapAction() {
         // Animate to show more map (closed position)
-        let kDefaultClosedMapPosition: CGFloat = 100.0
+        let positions = mapSheetPositions
         guard let contentViewTopConstraint = contentViewTopConstraint else { return }
         
         UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseOut) {
-            contentViewTopConstraint.constant = kDefaultClosedMapPosition
+            contentViewTopConstraint.constant = positions.closed
             self.view.layoutIfNeeded()
         }

420-427: Gate CTX merchant debug logging behind DEBUG flag.

Debug prints with merchant information should not appear in production logs.

+                #if DEBUG
                 // Debug logging for CTX merchant info
                 if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
                     print("🎯 CTX MERCHANT INFO DEBUG: \(pointOfUse.name)")
                     print("   CTX enabled: \(merchantInfo.enabled)")
                     print("   Local active: \(pointOfUse.active)")
                     print("   Will update view with enabled: \(merchantInfo.enabled)")
                 }
+                #endif

277-277: Avoid force unwrapping the contentViewTopConstraint.

Force unwrapping can cause crashes if the constraint is nil.

-            contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            contentViewTopConstraint = topConstraint
             
             constraint = [
-                contentViewTopConstraint!,
+                topConstraint,
                 contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

104-174: Consolidate map sheet positions and remove debug logging.

The sheet positions are computed inline multiple times with inconsistent values. Also, debug prints should be gated.

+    private var mapSheetPositions: (opened: CGFloat, half: CGFloat, closed: CGFloat) {
+        let height = view.frame.size.height
+        return (opened: height * 0.2, half: height * 0.5 - 72, closed: height * 0.75)
+    }
+    
     @objc
     private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
+        #if DEBUG
         print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
         guard let contentViewTopConstraint = contentViewTopConstraint else { 
+            #if DEBUG
             print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
             return 
         }
         
         let translatedPoint: CGPoint = sender.translation(in: view)
         let currentY = contentViewTopConstraint.constant
+        let positions = mapSheetPositions
         
         switch sender.state {
         case .changed:
             // Only handle vertical movement and constrain within bounds
             let newY = currentY + translatedPoint.y
             
-            // Constrain movement between closed position and maximum expanded position
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75 // Mostly closed (more map visible) 
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Default position higher by about an inch
-            let kDefaultOpenedMapPosition = screenHeight * 0.2 // Mostly open (less map visible, more content)
             // Allow dragging between open position (top) and closed position (bottom)
-            let maxY = kDefaultClosedMapPosition // Don't allow dragging below closed position
-            let minY = kDefaultOpenedMapPosition // Don't allow dragging above open position
+            let maxY = positions.closed
+            let minY = positions.opened
             
             contentViewTopConstraint.constant = max(minY, min(maxY, newY))
             sender.setTranslation(.zero, in: view)
             
         case .ended:
             let velocityInView = sender.velocity(in: view)
             let velocityY: CGFloat = velocityInView.y
             let finalCurrentY = contentViewTopConstraint.constant
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75 // Mostly closed (more map visible)
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72 // Higher by about an inch
-            let kDefaultOpenedMapPosition = screenHeight * 0.2
             
             var finalY: CGFloat
             
             if velocityY > 300 {
                 // Fast downward swipe - snap to closed position (more map visible)
-                finalY = kDefaultClosedMapPosition
+                finalY = positions.closed
             } else if velocityY < -300 {
                 // Fast upward swipe - snap to open position (less map visible)
-                finalY = kDefaultOpenedMapPosition
+                finalY = positions.opened
             } else {
                 // No strong velocity, snap to nearest position based on current position
-                let midPoint1 = (kDefaultOpenedMapPosition + kDefaultBottomHalfPosition) / 2
-                let midPoint2 = (kDefaultBottomHalfPosition + kDefaultClosedMapPosition) / 2
+                let midPoint1 = (positions.opened + positions.half) / 2
+                let midPoint2 = (positions.half + positions.closed) / 2
                 
                 if finalCurrentY < midPoint1 {
-                    finalY = kDefaultOpenedMapPosition
+                    finalY = positions.opened
                 } else if finalCurrentY < midPoint2 {
-                    finalY = kDefaultBottomHalfPosition
+                    finalY = positions.half
                 } else {
-                    finalY = kDefaultClosedMapPosition
+                    finalY = positions.closed
                 }
             }

36-36: Remove unused implicitly unwrapped optional property.

The showMapButton property is never initialized and is commented out in the code. This creates a crash risk if accidentally accessed.

     private var contentViewTopConstraint: NSLayoutConstraint?
-    private var showMapButton: UIButton!

Also remove the related unused methods setupMapButton() at line 192 and updateMapButtonVisibility() at line 231 since they reference this removed property.

🧹 Nitpick comments (8)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (6)

38-42: Let the formatter round; avoid pre-flooring distance.

floor(distance) can under-report by up to 0.99 m/ft before formatting. The formatter should handle rounding.

-            let distanceText: String = ExploreDash.distanceFormatter
-                .string(from: Measurement(value: floor(distance), unit: UnitLength.meters))
+            let distanceText: String = ExploreDash.distanceFormatter
+                .string(from: Measurement(value: distance, unit: .meters))

26-26: Remove unused distanceLabel property (and fix SwiftLint IUO warning).

distanceLabel is never shown and immediately hidden; drop it to simplify the view hierarchy and silence the IUO lint.

-    private var distanceLabel: UILabel!
+    // distanceLabel removed; distance is rendered via subLabel

47-49: Dead code: distanceLabel is always hidden and unused. Remove.

No UI effect; reduces noise.

-        // Hide separate distance label since we're using subLabel
-        distanceLabel.isHidden = true

67-75: Avoid adding a permanently hidden arranged subview.

Adding a hidden arranged subview increases complexity and can affect spacing. Remove this block (and the property) entirely.

-        // Distance label (separate from merchant name)
-        distanceLabel = UILabel()
-        distanceLabel.translatesAutoresizingMaskIntoConstraints = false
-        distanceLabel.font = .systemFont(ofSize: 12, weight: .regular)
-        distanceLabel.textColor = .dw_tertiaryText()
-        distanceLabel.isHidden = true
-        distanceLabel.textAlignment = .right
-        mainStackView.addArrangedSubview(distanceLabel)
+        // removed distanceLabel; subLabel is the single source of subtitle/distance

33-36: Minimize churn on legacy cells to reduce merge conflicts with feature/piggycards.

Avoid structural additions (like extra labels) unless needed; gate behavior behind a feature flag to ease future merges.

Also applies to: 67-75


33-45: Fall back to source when distance is unavailable
Mirror AtmItemCell by showing pointOfUse.source instead of hiding the subtitle:

-        } else {
-            subLabel.isHidden = true
-        }
+        } else if let source = pointOfUse.source, !source.isEmpty {
+            subLabel.isHidden = false
+            subLabel.text = source
+        } else {
+            subLabel.isHidden = true
+        }
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)

91-107: Refactor logout to propagate errors.

The .logout case is defined in CTXSpendEndpoint.swift, so the endpoint exists. Currently CTXSpendService.logout swallows any failures (catch { DSLogger.log(...) }) and always clears tokens without notifying callers. Change it to an async‐throws or Result-returning API so callers (e.g. CTXSpendUserAuthViewModel) can handle and surface logout errors, for example:

func logout() async throws {
    try await CTXSpendAPI.shared.request(.logout)
    await MainActor.run {
        KeychainService.delete(key: Keys.accessToken)
        
        updateSignInState()
    }
}
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

1085-1096: Remove debug logging from grabber pan gesture setup.

Debug prints should be gated for production builds.

     func setupGrabberPanGesture(target: Any, action: Selector) {
         guard let grabberContainer = grabberContainer else {
+            #if DEBUG
             print("DEBUG: grabberContainer is nil!")
+            #endif
             return
         }
+        #if DEBUG
         print("DEBUG: Setting up pan gesture on grabber container")
+        #endif
         let panRecognizer = UIPanGestureRecognizer(target: target, action: action)
         panRecognizer.minimumNumberOfTouches = 1
         panRecognizer.maximumNumberOfTouches = 1
         grabberContainer.addGestureRecognizer(panRecognizer)
+        #if DEBUG
         print("DEBUG: Pan gesture added to grabber container")
+        #endif
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 339a09f and 614a46a.

📒 Files selected for processing (7)
  • DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (3 hunks)
  • DashWallet/Sources/Models/Explore Dash/Services/CTXSpendEndpoint.swift (3 hunks)
  • DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (8 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (8 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Views/DashSpend/DashSpendPayViewModel.swift (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • DashWallet/Sources/Models/Explore Dash/Services/CTXSpendEndpoint.swift
  • DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is confirmed unused after code analysis. Radius functionality is properly handled through model.currentRadius and filters?.currentRadius, making the controller property redundant and safe to remove.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is unused and should be removed. Radius functionality is handled through the model.currentRadius and filters system instead of a controller-level property.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
🧬 Code graph analysis (4)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (2)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendAPI.swift (2)
  • request (82-90)
  • request (92-113)
DashWallet/Sources/UI/Explore Dash/Views/DashSpend/CTXSpendUserAuthViewModel.swift (1)
  • logout (83-85)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (5)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (2)
  • stopNetworkMonitoring (52-54)
  • startNetworkMonitoring (36-50)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)
  • callAction (87-102)
  • websiteAction (176-190)
DashWallet/Sources/Models/Explore Dash/Model/Entites/ExplorePointOfUse.swift (2)
  • toSavingPercentages (79-81)
  • isEnabled (88-90)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)
  • logout (90-108)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (4)
  • mapView (239-244)
  • mapView (246-256)
  • mapView (272-278)
  • configureHierarchy (203-233)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (2)
  • configureHierarchy (337-493)
  • showMap (109-120)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)
  • configureHierarchy (203-226)
  • payAction (182-196)
  • callAction (130-158)
  • setupGrabberPanGesture (1085-1096)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift (1)
  • update (41-49)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (1)
  • update (28-48)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 130-130: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 205-205: Prefer object literals over image and color inits

(object_literal)


[Warning] 276-276: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 382-382: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 350-350: Prefer object literals over image and color inits

(object_literal)


[Warning] 481-481: Prefer object literals over image and color inits

(object_literal)


[Warning] 547-547: Prefer object literals over image and color inits

(object_literal)


[Warning] 957-957: Prefer object literals over image and color inits

(object_literal)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1129-1129: Unimplemented functions should be marked as unavailable

(unavailable_function)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 36-36: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 55-55: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 277-277: Force unwrapping should be avoided

(force_unwrapping)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift

[Warning] 26-26: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)

- Fix AllMerchantLocationsViewController to respect current filter radius settings
- Add currentFilters parameter to AllMerchantLocationsDataProvider constructor
- Ensure "Show all locations" list matches the location count displayed
- Update PointOfUseDetailsView to fetch actual location count using same bounds calculation as list view
- Pass current filter settings through view hierarchy from MerchantListViewController to AllMerchantLocationsViewController

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

Co-Authored-By: Claude <[email protected]>
Copy link

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

Caution

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

⚠️ Outside diff range comments (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (1)

109-112: Fix typo in user-facing copy (“redius” → “radius”).

User-visible string has a spelling error.

Apply:

-        NSLocalizedString("Your location is used to show your position on the map, merchants in the selected redius and improve search results.",
+        NSLocalizedString("Your location is used to show your position on the map, merchants in the selected radius and improve search results.",
                           comment: "")
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)

246-248: Guard latitude/longitude to avoid crash.

-        mapView.initialCenterLocation = .init(latitude: pointOfUse.latitude!, longitude: pointOfUse.longitude!)
+        guard let lat = pointOfUse.latitude, let lon = pointOfUse.longitude else { return }
+        mapView.initialCenterLocation = .init(latitude: lat, longitude: lon)

375-377: Avoid force-unwrap on external URL.

-                UIApplication.shared.open(URL(string: CTXConstants.ctxGiftCardAgreementUrl)!, options: [:], completionHandler: nil)
+                if let url = URL(string: CTXConstants.ctxGiftCardAgreementUrl) {
+                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
+                }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

182-186: Normalize website URL (prepend https when missing).

-        guard let website = merchant.website, let url = URL(string: website) else { return }
-        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+        guard var website = merchant.website, !website.isEmpty else { return }
+        if !website.lowercased().hasPrefix("http") { website = "https://\(website)" }
+        guard let url = URL(string: website) else { return }
+        UIApplication.shared.open(url, options: [:], completionHandler: nil)
♻️ Duplicate comments (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)

67-67: Remove unused radius property (confirmed in prior review).

Radius is driven by model/filters; this controller property is redundant.

-    internal var radius = 5 // In miles - 5 mile radius as requested
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)

37-38: Delete unused IUO property.

Eliminate UIButton! to remove a crash path.

-    private var contentViewTopConstraint: NSLayoutConstraint?
-    private var showMapButton: UIButton!
+    private var contentViewTopConstraint: NSLayoutConstraint?

422-429: Gate CTX merchant debug prints behind DEBUG.

-                // Debug logging for CTX merchant info
-                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
+                #if DEBUG
+                // Debug logging for CTX merchant info
+                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
                     print("🎯 CTX MERCHANT INFO DEBUG: \(pointOfUse.name)")
                     print("   CTX enabled: \(merchantInfo.enabled)")
                     print("   Local active: \(pointOfUse.active)")
                     print("   Will update view with enabled: \(merchantInfo.enabled)")
                 }
+                #endif

272-280: Remove force unwrap of contentViewTopConstraint.

-            constraint = [
-                contentViewTopConstraint!,
+            guard let topConstraint = contentViewTopConstraint else { return }
+            constraint = [
+                topConstraint,
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

136-164: Gate phone debug logs to DEBUG (repeat of prior review).

-        print("DEBUG: Original phone: \(phone)")
+        #if DEBUG
+        print("DEBUG: Original phone: \(phone)")
+        #endif
@@
-        print("DEBUG: Extracted digits: \(digits)")
+        #if DEBUG
+        print("DEBUG: Extracted digits: \(digits)")
+        #endif
@@
-        print("DEBUG: URL string: \(urlString)")
+        #if DEBUG
+        print("DEBUG: URL string: \(urlString)")
+        #endif
@@
-            print("DEBUG: Failed to create URL")
+            #if DEBUG
+            print("DEBUG: Failed to create URL")
+            #endif
@@
-            print("DEBUG: Cannot open URL - phone not available")
+            #if DEBUG
+            print("DEBUG: Cannot open URL - phone not available")
+            #endif
@@
-        print("DEBUG: Opening URL: \(url)")
+        #if DEBUG
+        print("DEBUG: Opening URL: \(url)")
+        #endif
         UIApplication.shared.open(url, options: [:], completionHandler: { success in
-            print("DEBUG: URL open result: \(success)")
+            #if DEBUG
+            print("DEBUG: URL open result: \(success)")
+            #endif
         })
🧹 Nitpick comments (13)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (1)

207-215: Consider showing the count of physical merchants, not all items.

You guard on physicalMerchants.isEmpty but display items.count, which may include online entries.

-                return String(format: NSLocalizedString("%d merchant(s) in %@", comment: "#bc-ignore!"), items.count,
+                return String(format: NSLocalizedString("%d merchant(s) in %@", comment: "#bc-ignore!"), physicalMerchants.count,
                               ExploreDash.distanceFormatter
                                   .string(from: Measurement(value: model.currentRadius, unit: UnitLength.meters)))
-                return String(format: NSLocalizedString("%d merchant(s) in %@", comment: "#bc-ignore!"), items.count,
+                return String(format: NSLocalizedString("%d merchant(s) in %@", comment: "#bc-ignore!"), physicalMerchants.count,
                               ExploreDash.distanceFormatter
                                   .string(from: Measurement(value: model.currentRadiusMiles, unit: UnitLength.miles)))
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)

25-27: “Max 50% of screen height” comment is misleading for a fixed 320pt value.

Either remove the claim or compute it dynamically to truly cap at 50%.

Minimal fix (comment only):

-internal let kDefaultOpenedMapPosition: CGFloat = 320.0 // Max 50% of screen height
+internal let kDefaultOpenedMapPosition: CGFloat = 320.0 // Fixed height in points

Alternative (dynamic) outside this hunk:

// Add
internal func openedMapPosition(in view: UIView) -> CGFloat {
    min(view.bounds.height * 0.5, 320.0)
}
// Then replace uses of kDefaultOpenedMapPosition with openedMapPosition(in: self.view)

534-547: Drag bounds allow 80% but snap only to 320/closed → jarring jump.

Either clamp to 320 while dragging or add a third snap to the 80% position.

Option A (two-state): clamp to opened position during drag.

-            let maxY = view.frame.size.height * 0.8 // Allow dragging down to 80% of screen
+            let maxY = kDefaultOpenedMapPosition // Clamp to opened position

Option B (three-state): keep 80% drag and snap logic, but add a third snap when finalCurrentY is near maxY (outside this hunk).

Also applies to: 555-576


567-570: Fix SwiftLint vertical parameter alignment.

Align multiline parameters.

-            UIView.animate(withDuration: animationDuration, delay: 0, 
-                          usingSpringWithDamping: 0.8, initialSpringVelocity: 0, 
-                          options: .curveEaseOut) {
+            UIView.animate(withDuration: animationDuration,
+                           delay: 0,
+                           usingSpringWithDamping: 0.8,
+                           initialSpringVelocity: 0,
+                           options: .curveEaseOut) {
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (2)

62-70: Cache check should include lastUserPoint for correctness.

Same query/bounds but different userPoint can change distance ordering.

-        if lastQuery == query && !items.isEmpty && lastBounds == finalBounds {
+        if lastQuery == query && !items.isEmpty && lastBounds == finalBounds && lastUserPoint?.latitude == finalUserPoint?.latitude && lastUserPoint?.longitude == finalUserPoint?.longitude {
             completion(.success(items))
             return
         }

84-91: Filtering to active locations only: confirm intended UX.

If “Temporarily unavailable” is represented by active=false or enabled=false, this filter may hide locations users still expect to see (but disabled to buy). Verify.

Would you like me to add unit tests covering:

  • authorized vs denied vs needsAuthorization startup states
  • filtersToUse radius driving bounds
  • active vs inactive location visibility?
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)

54-58: Clamp negative map contentInset.

On small screens, bottom inset can go negative. Clamp to ≥ 0.

-            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: mapView.frame.height - detailsView.frame.height - 10,
-                                               right: 0)
+            let bottom = max(0, mapView.frame.height - detailsView.frame.height - 10)
+            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0)

106-113: Avoid shadowing and gate debug prints.

Rename the local, and keep prints out of release builds.

-        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
-        guard let contentViewTopConstraint = contentViewTopConstraint else { 
-            print("DEBUG: contentViewTopConstraint is nil!")
+        #if DEBUG
+        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
+        guard let topConstraint = contentViewTopConstraint else {
+            #if DEBUG
+            print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
             return 
         }
-        let currentY = contentViewTopConstraint.constant
+        let currentY = topConstraint.constant

123-131: Unify sheet positions; remove magic numbers.

Compute once per frame and reuse to reduce drift across code paths.

-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72
-            let kDefaultOpenedMapPosition = screenHeight * 0.2
-            let maxY = kDefaultClosedMapPosition
-            let minY = kDefaultOpenedMapPosition
-            contentViewTopConstraint.constant = max(minY, min(maxY, newY))
+            let pos = mapPositions()
+            let maxY = pos.closed
+            let minY = pos.opened
+            topConstraint.constant = max(minY, min(maxY, newY))
@@
-            let finalCurrentY = contentViewTopConstraint.constant
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75
-            let kDefaultBottomHalfPosition = screenHeight * 0.5 - 72
-            let kDefaultOpenedMapPosition = screenHeight * 0.2
+            let finalCurrentY = topConstraint.constant
+            let pos = mapPositions()
@@
-                finalY = kDefaultClosedMapPosition
+                finalY = pos.closed
@@
-                finalY = kDefaultOpenedMapPosition
+                finalY = pos.opened
@@
-                let midPoint1 = (kDefaultOpenedMapPosition + kDefaultBottomHalfPosition) / 2
-                let midPoint2 = (kDefaultBottomHalfPosition + kDefaultClosedMapPosition) / 2
+                let midPoint1 = (pos.opened + pos.half) / 2
+                let midPoint2 = (pos.half + pos.closed) / 2
@@
-                    finalY = kDefaultOpenedMapPosition
+                    finalY = pos.opened
@@
-                    finalY = kDefaultBottomHalfPosition
+                    finalY = pos.half
@@
-                    finalY = kDefaultClosedMapPosition
+                    finalY = pos.closed
@@
-                contentViewTopConstraint.constant = finalY
+                topConstraint.constant = finalY

Add helper (outside changed hunk, near other privates):

private func mapPositions() -> (opened: CGFloat, half: CGFloat, closed: CGFloat) {
    let h = view.bounds.height
    return (opened: h * 0.2, half: h * 0.5 - 72, closed: h * 0.75)
}

Also applies to: 139-163


221-231: Replace fixed 100.0 with unified closed position.

-        let kDefaultClosedMapPosition: CGFloat = 100.0
-        guard let contentViewTopConstraint = contentViewTopConstraint else { return }
+        let pos = mapPositions()
+        guard let topConstraint = contentViewTopConstraint else { return }
@@
-            contentViewTopConstraint.constant = kDefaultClosedMapPosition
+            topConstraint.constant = pos.closed
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)

1065-1071: Localize “away”.

Keep UI strings localized.

-            return "\(address)\n\(distanceText) away"
+            let format = NSLocalizedString("%@ away", comment: "Distance suffix")
+            return "\(address)\n" + String(format: format, distanceText)

569-583: Wrap noisy DEBUG_FETCH_ logs in DEBUG.*

These logs are extensive; keep them out of releases.

Example for the first block:

-        print("DEBUG_FETCH_COUNT: Starting location fetch for merchant: '\(merchant.name)'")
+        #if DEBUG
+        print("DEBUG_FETCH_COUNT: Starting location fetch for merchant: '\(merchant.name)'")
         print("DEBUG_FETCH_COUNT: pointOfUseId: '\(merchant.pointOfUseId)'")
         if let merchantData = merchant.merchant {
             print("DEBUG_FETCH_COUNT: merchantId: '\(merchantData.merchantId)'")
         }
         print("DEBUG_FETCH_COUNT: merchant.active: \(merchant.active)")
+        #endif

Apply similarly to the remaining DEBUG_FETCH_* prints in this method and in updateShowAllLocationsButton().

Also applies to: 624-639, 650-657, 660-674, 676-676


1206-1216: Gate grabber pan debug prints behind DEBUG.

-        guard let grabberContainer = grabberContainer else {
-            print("DEBUG: grabberContainer is nil!")
+        guard let grabberContainer = grabberContainer else {
+            #if DEBUG
+            print("DEBUG: grabberContainer is nil!")
+            #endif
             return
         }
-        print("DEBUG: Setting up pan gesture on grabber container")
+        #if DEBUG
+        print("DEBUG: Setting up pan gesture on grabber container")
+        #endif
@@
-        print("DEBUG: Pan gesture added to grabber container")
+        #if DEBUG
+        print("DEBUG: Pan gesture added to grabber container")
+        #endif
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 614a46a and 851e794.

📒 Files selected for processing (7)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (10 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (3 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (4 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is confirmed unused after code analysis. Radius functionality is properly handled through model.currentRadius and filters?.currentRadius, making the controller property redundant and safe to remove.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is unused and should be removed. Radius functionality is handled through the model.currentRadius and filters system instead of a controller-level property.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
🧬 Code graph analysis (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (2)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • items (38-39)
  • items (42-175)
  • allLocations (200-247)
DashWallet/Sources/Models/Explore Dash/ExploreDash.swift (1)
  • allLocations (189-192)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)
  • callAction (89-104)
  • websiteAction (178-192)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (2)
  • stopNetworkMonitoring (52-54)
  • startNetworkMonitoring (36-50)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • allLocations (200-247)
  • items (38-39)
  • items (42-175)
DashWallet/Sources/Models/Explore Dash/ExploreDash.swift (1)
  • allLocations (189-192)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)
  • items (32-74)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)
  • logout (90-108)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (6)
  • tableView (628-684)
  • tableView (686-702)
  • tableView (708-725)
  • tableView (727-738)
  • tableView (740-762)
  • tableView (764-768)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (3)
  • tableView (117-144)
  • tableView (146-171)
  • tableView (173-186)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (3)
  • mapView (239-244)
  • mapView (246-256)
  • mapView (272-278)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (4)
  • mapView (239-244)
  • mapView (246-256)
  • mapView (272-278)
  • configureHierarchy (203-233)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (2)
  • configureHierarchy (337-493)
  • showMap (109-120)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)
  • configureHierarchy (209-232)
  • callAction (136-164)
  • websiteAction (182-186)
  • setupGrabberPanGesture (1205-1216)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (1)
  • configureHierarchy (75-82)
DashWallet/Sources/Categories/UIHostingController+DashWallet.swift (1)
  • setDetent (21-31)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 136-136: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 112-112: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 126-126: Unimplemented functions should be marked as unavailable

(unavailable_function)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 211-211: Prefer object literals over image and color inits

(object_literal)


[Warning] 282-282: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 388-388: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 356-356: Prefer object literals over image and color inits

(object_literal)


[Warning] 450-450: Prefer object literals over image and color inits

(object_literal)


[Warning] 516-516: Prefer object literals over image and color inits

(object_literal)


[Warning] 1077-1077: Prefer object literals over image and color inits

(object_literal)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1249-1249: Unimplemented functions should be marked as unavailable

(unavailable_function)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift

[Warning] 568-568: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 569-569: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 37-37: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 57-57: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 279-279: Force unwrapping should be avoided

(force_unwrapping)

🔇 Additional comments (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (1)

193-196: All PointOfUseDetailsViewController initializers include currentFilters Verified call sites in MerchantListViewController.swift and ExplorePointOfUseListViewController.swift; approving changes.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (3)

25-31: Initializer now accepts currentFilters: looks good.

Plumbs filter context down cleanly.


50-57: Hiding segments in this screen: good choice.

Delegating .filters and .search row counts to super preserves layout.


69-73: Data provider wiring with currentFilters: good.

Aligned with the list → details → all-locations flow.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)

142-149: Details VC construction with currentFilters: good.

Keeps filter state when drilling into details.

- Fix location permission handling to show global locations when services denied/unavailable
- Add comprehensive debug logging with proper string formatting
- Replace magic numbers with named constants for better maintainability
- Fix typo: "redius" → "radius" in location service popup messages
- Improve AllMerchantLocationsDataProvider filter bounds calculation

Resolves issues where:
- Location counts showed 0 on devices without location permission
- AMC and other merchants showed blank location lists
- Debug logging caused compilation errors

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

Co-Authored-By: Claude <[email protected]>
Copy link

@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 (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AtmListViewController.swift (4)

20-22: Rename AtmListSegmnetsAtmListSegments across AtmListViewController.swift
Update the enum declaration, MARK comment, extension and all call sites in this file.

--- AtmListViewController.swift
- // MARK: - AtmListSegmnets
+ // MARK: - AtmListSegments
- enum AtmListSegmnets: Int {
+ enum AtmListSegments: Int {
...
- extension AtmListSegmnets {
+ extension AtmListSegments {
...
- AtmListSegmnets.all.pointOfUseListSegment
+ AtmListSegments.all.pointOfUseListSegment

28-31: Move cross-type == overloads to file scope

  • In AtmListViewController.swift (lines 28–31), remove the member overload
    -    static func ==(lhs: PointOfUseListSegment, rhs: AtmListSegmnets) -> Bool {
    -        lhs.tag == rhs.rawValue
    -    }
    and add at file scope:
    func ==(lhs: PointOfUseListSegment, rhs: AtmListSegmnets) -> Bool { lhs.tag == rhs.rawValue }
    func ==(lhs: AtmListSegmnets, rhs: PointOfUseListSegment) -> Bool { rhs == lhs }
  • In MerchantListViewController.swift (lines 30–32), remove the member overload
    -    static func ==(lhs: PointOfUseListSegment, rhs: MerchantsListSegment) -> Bool {
    -        lhs.tag == rhs.rawValue
    -    }
    and add at file scope:
    func ==(lhs: PointOfUseListSegment, rhs: MerchantsListSegment) -> Bool { lhs.tag == rhs.rawValue }
    func ==(lhs: MerchantsListSegment, rhs: PointOfUseListSegment) -> Bool { rhs == lhs }
  • Keep the same-type static func ==(PointOfUseListSegment, PointOfUseListSegment) in PointOfUseListModel.swift for Equatable conformance.

33-35: Remove unused defaultFilters
In AtmListViewController (lines 33–35), drop the two dead-code lines:

var defaultFilters = PointOfUseListFilters()
defaultFilters.radius = .twenty

61-65: Implement missing SellAtmsDataProvider and fix .sell mapping
.sell currently returns BuyAndSellAtmsDataProvider(), duplicating the .buyAndSell case. There is no SellAtmsDataProvider class—add one (subclassing BaseAtmsDataProvider) and change the .sell branch to return it. (AtmListViewController.swift lines 61–65)

♻️ Duplicate comments (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (2)

136-164: Gate phone debug prints behind DEBUG.

Avoid leaking to production logs. Keep functionality unchanged.

   @objc func callAction() {
     guard let phone = merchant.phone, !phone.isEmpty else { return }
-    print("DEBUG: Original phone: \(phone)")
+    #if DEBUG
+    print("DEBUG: Original phone: \(phone)")
+    #endif
@@
-    let digits = phone.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
-    guard !digits.isEmpty else { return }
-    print("DEBUG: Extracted digits: \(digits)")
+    let digits = phone.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
+    guard !digits.isEmpty else { return }
+    #if DEBUG
+    print("DEBUG: Extracted digits: \(digits)")
+    #endif
@@
-    let urlString = "telprompt:\(digits)"
-    print("DEBUG: URL string: \(urlString)")
+    let urlString = "telprompt:\(digits)"
+    #if DEBUG
+    print("DEBUG: URL string: \(urlString)")
+    #endif
@@
-    guard let url = URL(string: urlString) else {
-        print("DEBUG: Failed to create URL")
-        return
-    }
+    guard let url = URL(string: urlString) else {
+        #if DEBUG
+        print("DEBUG: Failed to create URL")
+        #endif
+        return
+    }
@@
-    guard UIApplication.shared.canOpenURL(url) else {
-        print("DEBUG: Cannot open URL - phone not available")
-        return
-    }
+    guard UIApplication.shared.canOpenURL(url) else {
+        #if DEBUG
+        print("DEBUG: Cannot open URL - phone not available")
+        #endif
+        return
+    }
@@
-    print("DEBUG: Opening URL: \(url)")
-    UIApplication.shared.open(url, options: [:], completionHandler: { success in
-        print("DEBUG: URL open result: \(success)")
-    })
+    #if DEBUG
+    print("DEBUG: Opening URL: \(url)")
+    #endif
+    UIApplication.shared.open(url, options: [:], completionHandler: { success in
+        #if DEBUG
+        print("DEBUG: URL open result: \(success)")
+        #endif
+    })
   }

1141-1156: Gate CTX merchant field dumps and GameStop denomination logs behind DEBUG (or remove).

These prints expose merchant identifiers and should not appear in release builds.

-        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") || self.merchant.name.lowercased().contains("buffalo") {
+        #if DEBUG
+        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") || self.merchant.name.lowercased().contains("buffalo") {
             print("🎯 MERCHANT DEBUG: \(self.merchant.name)")
@@
             print("   ----------")
         }
+        #endif
-            // Debug denominationsType for GameStop specifically
-            if self.merchant.name.lowercased().contains("gamestop") {
+            // Debug denominationsType for GameStop specifically
+            #if DEBUG
+            if self.merchant.name.lowercased().contains("gamestop") {
                 print("🎯 GAMESTOP DENOMINATIONS TYPE DEBUG:")
                 print("   Original denominationsType: '\(denominationsType)'")
                 print("   Lowercased denominationsType: '\(lowercasedType)'")
             }
+            #endif
@@
-            case "fixed":
-                if self.merchant.name.lowercased().contains("gamestop") {
-                    print("   → Matched 'fixed' case - returning Fixed amounts")
-                }
+            case "fixed":
                 return NSLocalizedString("Fixed amounts", comment: "DashSpend")
-            case "flexible", "min-max":
-                if self.merchant.name.lowercased().contains("gamestop") {
-                    print("   → Matched 'flexible'/'min-max' case - returning Flexible amounts")
-                }
+            case "flexible", "min-max":
                 return NSLocalizedString("Flexible amounts", comment: "DashSpend")
-            default:
-                if self.merchant.name.lowercased().contains("gamestop") {
-                    print("   → Matched 'default' case - returning Fixed amounts")
-                }
+            default:
                 return NSLocalizedString("Fixed amounts", comment: "DashSpend")

Also applies to: 1163-1185

🧹 Nitpick comments (12)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AtmListViewController.swift (2)

92-100: Improve i18n: pluralization and format

"%d ATM(s) in %@" is not localizable for plural rules. Use a .stringsdict entry and String.localizedStringWithFormat.

Example:

  • Localized key: “ATM_COUNT_IN_DISTANCE” → Strings: “%#@ATM_COUNT@ in %@”
  • stringsdict plural for ATM_COUNT with one/other forms (“%d ATM” / “%d ATMs”).
    Swift:
let count = items.count
let distance = ExploreDash.distanceFormatter.string(from: Measurement(value: model.currentRadiusMiles, unit: .miles))
return String(format: NSLocalizedString("ATM_COUNT_IN_DISTANCE", comment: ""), count, distance)

116-116: Remove stray semicolon

Swift doesn’t need it; keeping style consistent helps.

-        title = NSLocalizedString("ATMs", comment: "");
+        title = NSLocalizedString("ATMs", comment: "")
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (10)

182-186: Normalize website URL (add https:// if missing).

Prevents tap failures when merchant.website lacks a scheme.

-    guard let website = merchant.website, let url = URL(string: website) else { return }
-    UIApplication.shared.open(url, options: [:], completionHandler: nil)
+    guard let website = merchant.website else { return }
+    let normalizedWebsite: String
+    if website.hasPrefix("http://") || website.hasPrefix("https://") {
+        normalizedWebsite = website
+    } else {
+        normalizedWebsite = "https://" + website
+    }
+    guard let url = URL(string: normalizedWebsite) else { return }
+    UIApplication.shared.open(url, options: [:], completionHandler: nil)

210-226: Use dynamic/system colors for background (Dark Mode).

Hardcoded RGB will look off in Dark Mode. Prefer theme color.

-        backgroundColor = UIColor(red: 0.961, green: 0.965, blue: 0.969, alpha: 1) // #f5f6f7
+        backgroundColor = .dw_secondaryBackground()

360-360: Adopt Dynamic Type fonts.

Use app typography helpers for scaling instead of fixed 14pt.

-        payButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) // 14px as per Figma
+        payButton.titleLabel?.font = .dw_font(forTextStyle: .footnote)
+        payButton.titleLabel?.adjustsFontForContentSizeCategory = true
-        addressTextLabel.font = .systemFont(ofSize: 14, weight: .regular) // 14px regular as per Figma
+        addressTextLabel.font = .dw_font(forTextStyle: .body)
-        phoneButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .regular) // 14px as per Figma
+        phoneButton.titleLabel?.font = .dw_font(forTextStyle: .body)

Also applies to: 449-451, 517-517


935-957: Show the U.S.-only notice conditionally.

Display only for US merchants or US device region.

         let noticeLabel = UILabel()
         noticeLabel.translatesAutoresizingMaskIntoConstraints = false
         noticeLabel.text = NSLocalizedString("Note: This card works only in the United States.", comment: "DashSpend")
@@
         container.addSubview(noticeLabel)
+        // Only show for US-context merchants
+        let isUSMerchant = (merchant.territory == "US")
+        let isUSDevice = Locale.current.regionCode == "US"
+        container.isHidden = !(isUSMerchant || isUSDevice)

1019-1026: Localize “Physical Merchant” strings.

These are user-facing but not localized.

-                return m.type == .onlineAndPhysical ? "Physical Merchant, Online" : "Physical Merchant"
+                return m.type == .onlineAndPhysical
+                    ? NSLocalizedString("Physical Merchant, Online", comment: "Merchant type")
+                    : NSLocalizedString("Physical Merchant", comment: "Merchant type")

1078-1097: Localize “away” suffix.

Use a format string for “%{distance} away”.

-            return "\(address)\n\(distanceText) away"
+            let awayText = String.localizedStringWithFormat(NSLocalizedString("%@ away", comment: "Distance away"), distanceText)
+            return "\(address)\n\(awayText)"
-            let distanceString = NSAttributedString(string: "\n\(distanceText) away", attributes: distanceAttributes)
+            let awayText = String.localizedStringWithFormat(NSLocalizedString("%@ away", comment: "Distance away"), distanceText)
+            let distanceString = NSAttributedString(string: "\n\(awayText)", attributes: distanceAttributes)

Also applies to: 1119-1129


569-592: Remove or gate verbose DEBUG_FETCH_ prints and grabber logs.*

Too noisy for production; also leaks merchant info.

-        print("DEBUG_FETCH_COUNT: Starting location fetch for merchant: '\(merchant.name)'")
+        #if DEBUG
+        print("DEBUG_FETCH_COUNT: Starting location fetch for merchant: '\(merchant.name)'")
@@
-        print("DEBUG_FETCH_RESULTS: Location denied/not authorized, fetching all locations globally")
+        print("DEBUG_FETCH_RESULTS: Location denied/not authorized, fetching all locations globally")
@@
-        print("DEBUG_FETCH_COUNT: Final API call parameters:")
+        print("DEBUG_FETCH_COUNT: Final API call parameters:")
@@
-                        print("DEBUG_FETCH_RESULTS: Results for '\(strongSelf.merchant.name)'")
+                        print("DEBUG_FETCH_RESULTS: Results for '\(strongSelf.merchant.name)'")
@@
-                    print("DEBUG_FETCH_RESULTS: Setting locationCount to \(newCount)")
+                    print("DEBUG_FETCH_RESULTS: Setting locationCount to \(newCount)")
@@
-                print("DEBUG_FETCH_RESULTS: API call failed with error: \(error)")
+                print("DEBUG_FETCH_RESULTS: API call failed with error: \(error)")
@@
-        print("DEBUG_FETCH_RESULTS: updateShowAllLocationsButton called with locationCount: \(locationCount)")
+        print("DEBUG_FETCH_RESULTS: updateShowAllLocationsButton called with locationCount: \(locationCount)")
@@
-            print("DEBUG: grabberContainer is nil!")
+            print("DEBUG: grabberContainer is nil!")
@@
-        print("DEBUG: Setting up pan gesture on grabber container")
+        print("DEBUG: Setting up pan gesture on grabber container")
@@
-        print("DEBUG: Pan gesture added to grabber container")
+        print("DEBUG: Pan gesture added to grabber container")
+        #endif

If you prefer, replace with DSLogger.debug(...) which is compiled out in release builds.

Also applies to: 612-622, 625-633, 645-650, 654-659, 661-668, 676-704, 1232-1242


673-704: Avoid brittle subview scanning; store a reference to the button.

Keep a weak property and set its title directly.

-        // Find and update the "Show all locations" button text
-        // This is called after the location count is fetched
-        print("DEBUG_FETCH_RESULTS: updateShowAllLocationsButton called with locationCount: \(locationCount)")
-        guard !isShowAllHidden, let containerView = containerView else { 
-            print("DEBUG_FETCH_RESULTS: Guard failed - isShowAllHidden: \(isShowAllHidden), containerView exists: \(containerView != nil)")
-            return 
-        }
-        
-        print("DEBUG_FETCH_RESULTS: Container has \(containerView.arrangedSubviews.count) arranged subviews")
-        
-        // Find the show all locations block (should be the last arranged subview)
-        if let showAllBlock = containerView.arrangedSubviews.last {
-            print("DEBUG_FETCH_RESULTS: Found show all block with \(showAllBlock.subviews.count) subviews")
-            
-            // Find the button within the block
-            for (index, subview) in showAllBlock.subviews.enumerated() {
-                print("DEBUG_FETCH_RESULTS: Checking subview \(index): \(type(of: subview))")
-                if let button = subview as? UIButton {
-                    let newTitle = String.localizedStringWithFormat(
-                        NSLocalizedString("Show all locations (%d)", comment: "Show all locations with count"), 
-                        locationCount
-                    )
-                    print("DEBUG_FETCH_RESULTS: Updating button title to: \(newTitle)")
-                    button.setTitle(newTitle, for: .normal)
-                    break
-                }
-            }
-        } else {
-            print("DEBUG_FETCH_RESULTS: No show all block found in container")
-        }
+        guard !isShowAllHidden, let button = showAllLocationsButton else { return }
+        let newTitle = String.localizedStringWithFormat(
+            NSLocalizedString("Show all locations (%d)", comment: "Show all locations with count"),
+            locationCount
+        )
+        button.setTitle(newTitle, for: .normal)
-        let showAllButton = UIButton()
+        let showAllButton = UIButton()
         showAllButton.translatesAutoresizingMaskIntoConstraints = false
@@
         showAllButton.addTarget(self, action: #selector(showAllLocationsAction), for: .touchUpInside)
+        self.showAllLocationsButton = showAllButton

Outside this hunk, add:

// Add near other stored properties
private weak var showAllLocationsButton: UIButton?

Also applies to: 706-745


35-41: Fix access control and closure signatures (SwiftLint).

Class is internal, but members/initializer are public; also prefer () -> Void.

-    public var payWithDashHandler: (()->())?
-    public var sellDashHandler: (()->())?
-    public var dashSpendAuthHandler: (()->())?
-    public var buyGiftCardHandler: (()->())?
-    public var showAllLocationsActionBlock: (() -> ())?
-    public var infoButtonActionBlock: (() -> ())? // For parent view controller to handle info button tap
+    var payWithDashHandler: (() -> Void)?
+    var sellDashHandler: (() -> Void)?
+    var dashSpendAuthHandler: (() -> Void)?
+    var buyGiftCardHandler: (() -> Void)?
+    var showAllLocationsActionBlock: (() -> Void)?
+    var infoButtonActionBlock: (() -> Void)? // For parent view controller to handle info button tap
-    public init(merchant: ExplorePointOfUse, isShowAllHidden: Bool = false, currentFilters: PointOfUseListFilters? = nil) {
+    init(merchant: ExplorePointOfUse, isShowAllHidden: Bool = false, currentFilters: PointOfUseListFilters? = nil) {

Also applies to: 112-116


47-48: Remove unused coverImageView. Declared in PointOfUseDetailsView.swift but never referenced—delete its declaration or wire it up.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 851e794 and 68d9c7d.

📒 Files selected for processing (4)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AtmListViewController.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift
🧰 Additional context used
🧬 Code graph analysis (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)
  • callAction (89-104)
  • websiteAction (178-192)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • allLocations (200-247)
  • items (38-39)
  • items (42-175)
DashWallet/Sources/Models/Explore Dash/ExploreDash.swift (1)
  • allLocations (189-192)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)
  • items (32-73)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 136-136: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 112-112: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 126-126: Unimplemented functions should be marked as unavailable

(unavailable_function)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 211-211: Prefer object literals over image and color inits

(object_literal)


[Warning] 282-282: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 388-388: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 356-356: Prefer object literals over image and color inits

(object_literal)


[Warning] 450-450: Prefer object literals over image and color inits

(object_literal)


[Warning] 516-516: Prefer object literals over image and color inits

(object_literal)


[Warning] 1103-1103: Prefer object literals over image and color inits

(object_literal)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1275-1275: Unimplemented functions should be marked as unavailable

(unavailable_function)

🔇 Additional comments (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AtmListViewController.swift (1)

85-88: LGTM: copy fix and explicit NSLocalizedString comment

“radius” typo correction and explicit comment parameter look good.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

1-1: No embedded Uphold secrets found Info.plist references CLIENT_ID/CLIENT_SECRET via build variables, no plaintext credentials present.

…tioning

- Add dynamic location filtering based on visible map area when users zoom/pan
- Update ExploreMapView to use actual visible bounds instead of hardcoded radius
- Pass current map bounds through view hierarchy for consistent filtering
- Enhance PointOfUseDetailsView to prioritize map bounds over filter radius
- Update AllMerchantLocationsDataProvider to support dynamic map bounds
- Adjust default dragger position to ensure "Show all locations" button is fully visible
- Maintain 1-mile zoom level centered on selected merchant location

Features:
- Default behavior: Uses filter radius settings (5 miles, 20 miles, etc.)
- Map interaction: Switches to actual visible map area for more intuitive results
- Real-time updates: Results and counts update as users move the map
- Improved UX: "Show all locations" button now fully visible by default

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

Co-Authored-By: Claude <[email protected]>
Copy link

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

Caution

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

⚠️ Outside diff range comments (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (1)

49-55: Center computation bug: use multiplication for Bx (spherical midpoint)

bX must be cos(lat2) * cos(dLon); using + yields incorrect centers for many rectangles.

-        let bX = cos(lat2) + cos(dLon)
+        let bX = cos(lat2) * cos(dLon)

Optional: for MapKit-derived rects, a simpler and precise alternative is Mercator midpoint:
MKMapPoint(x:(ne.x+sw.x)/2, y:(ne.y+sw.y)/2).coordinate.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)

499-508: Bug: you compute filterGroups but pass the unfiltered groups to the view.

This ignores the location-aware filter restriction.

Apply:

-        let filtersView = MerchantFiltersView(
-            currentFilters: model.filters,
-            filterGroups: currentSegment.filterGroups,
+        let filtersView = MerchantFiltersView(
+            currentFilters: model.filters,
+            filterGroups: filterGroups,
             territoriesDataSource: currentSegment.territoriesDataSource,
             sortOptions: currentSegment.sortOptions
         ) { [weak self] filters in
♻️ Duplicate comments (13)
DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (3)

24-26: Authorize URL must include OAuth2 essentials (response_type, redirect_uri, PKCE) and avoid hardcoded scopes in a static string.

Build the URL dynamically and pass PKCE params; the current format risks auth failures and prevents least-privilege scopes.

Apply:

-+ (NSString *)authorizeURLFormat {
-    return @"https://wallet.uphold.com/authorize/c184650d0cb44e73d8e5cb2021753a721c41f74a?scope=accounts:read%20cards:read%20cards:write%20transactions:deposit%20transactions:read%20transactions:transfer:application%20transactions:transfer:others%20transactions:transfer:self%20transactions:withdraw%20transactions:commit:otp%20user:read&state=%@";
-}
++ (NSString *)authorizeURLWithState:(NSString *)state
+                       redirectURI:(NSString *)redirectURI
+                     codeChallenge:(NSString *)codeChallenge
+            codeChallengeMethodS256:(BOOL)useS256
+                             scopes:(NSArray<NSString *> *)scopes {
+    NSString *scope = [scopes componentsJoinedByString:@"%20"];
+    NSString *method = useS256 ? @"S256" : @"plain";
+    return [NSString stringWithFormat:
+            @"https://wallet.uphold.com/authorize/%@?response_type=code&redirect_uri=%@&scope=%@&state=%@&code_challenge=%@&code_challenge_method=%@",
+            [self clientID],
+            [redirectURI stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet],
+            scope,
+            state,
+            codeChallenge,
+            method];
+}

Be sure to update call sites (e.g., DWUpholdClient) to generate PKCE and pass minimal scopes.


32-38: Hardcoded OAuth credentials; remove secret, inject client ID, and rotate immediately.

Client secrets must not ship in mobile apps; consider the secret compromised. Use PKCE and rotate in Uphold.

Apply:

 + (NSString *)clientID {
-    return @"c184650d0cb44e73d8e5cb2021753a721c41f74a";
+    NSString *value = [NSBundle.mainBundle objectForInfoDictionaryKey:@"UPHOLD_CLIENT_ID"];
+    NSAssert(value.length > 0, @"Missing UPHOLD_CLIENT_ID in Info.plist");
+    return value;
 }
 
-+ (NSString *)clientSecret {
-    return @"da72feee8236f7709df6d0c235a8896ad45f2a91";
-}
+// Public clients must not embed a client secret. Use OAuth 2.0 PKCE.

Follow-ups:

  • Purge the secret from git history and rotate on Uphold.
  • Replace any Basic Auth/token exchange using clientSecret with PKCE.

Run to find any remaining occurrences:

#!/bin/bash
rg -n --hidden -S 'clientSecret|UPHOLD_CLIENT_SECRET|UPHOLD_CLIENT_ID|oauth2/revoke' -g '!**/Pods/**' -C2

48-50: Add token revocation endpoint; homepage/dashboard isn’t a logout.

Expose /oauth2/revoke and ensure callers POST to it, then clear local tokens; optionally navigate to dashboard.

Apply:

 + (NSString *)logoutURLString {
     return @"https://wallet.uphold.com/dashboard";
 }
 
+// Revoke OAuth tokens (POST Authorization: Bearer ...).
++ (NSString *)revokeTokenURLString {
+    return @"https://api.uphold.com/oauth2/revoke";
+}

Also declare in the header and update the logout flow to call revoke before presenting the logout URL.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)

67-67: Remove unused radius property.

Radius is managed by filters/model; this stored property is dead and confusing.

Apply:

-internal var radius = 5 // In miles - 5 mile radius as requested
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (5)

40-40: Fix access level mismatch.

The initializer is declared public while the containing class is internal, causing a compilation error.

Apply this fix:

-    public init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, currentFilters: PointOfUseListFilters? = nil, currentMapBounds: ExploreMapBounds? = nil) {
+    init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, currentFilters: PointOfUseListFilters? = nil, currentMapBounds: ExploreMapBounds? = nil) {

38-38: Remove unused IUO property that creates crash risk.

The showMapButton property is declared as implicitly unwrapped optional but is never properly initialized. This creates potential crash paths.

Apply this fix:

-    private var showMapButton: UIButton!

Also remove the related dead code in setupMapButton(), showMapAction(), and updateMapButtonVisibility() methods since map button functionality appears to be disabled.


281-281: Avoid force unwrapping constraint.

The force unwrap of contentViewTopConstraint can crash if the constraint is nil.

Apply this fix:

-            contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
-            
-            constraint = [
-                contentViewTopConstraint!,
+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            contentViewTopConstraint = topConstraint
+            
+            constraint = [
+                topConstraint,

424-430: Debug prints should be gated behind DEBUG.

These debug prints contain merchant identifiers and will appear in production logs.

Apply this fix:

-                // Debug logging for CTX merchant info
-                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
+                #if DEBUG
+                // Debug logging for CTX merchant info
+                if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
                     print("🎯 CTX MERCHANT INFO DEBUG: \(pointOfUse.name)")
                     print("   CTX enabled: \(merchantInfo.enabled)")
                     print("   Local active: \(pointOfUse.active)")
                     print("   Will update view with enabled: \(merchantInfo.enabled)")
                 }
+                #endif

108-178: Unify sheet position constants to prevent inconsistencies.

The pan gesture handler duplicates position calculations that are computed differently across methods (e.g., showMapAction uses 100.0 while this method uses percentage-based calculations). This can lead to inconsistent behavior.

Create a centralized helper method:

+    // Consistent sheet positions
+    private func sheetPositions() -> (opened: CGFloat, half: CGFloat, closed: CGFloat) {
+        let h = view.bounds.height
+        return (opened: h * 0.2, half: h * 0.35, closed: h * 0.75)
+    }
+    
     private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
-        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #if DEBUG
+        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
         guard let contentViewTopConstraint = contentViewTopConstraint else { 
-            print("DEBUG: contentViewTopConstraint is nil!")
+            #if DEBUG
+            print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
             return 
         }
         
@@ -124,10 +130,7 @@ extension PointOfUseDetailsViewController {
             let newY = currentY + translatedPoint.y
             
             // Constrain movement between closed position and maximum expanded position
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75 // Mostly closed (more map visible) 
-            let kDefaultBottomHalfPosition = screenHeight * 0.35 // Default position to show content including button
-            let kDefaultOpenedMapPosition = screenHeight * 0.2 // Mostly open (less map visible, more content)
+            let pos = sheetPositions()
             // Allow dragging between open position (top) and closed position (bottom)
-            let maxY = kDefaultClosedMapPosition // Don't allow dragging below closed position
-            let minY = kDefaultOpenedMapPosition // Don't allow dragging above open position
+            let maxY = pos.closed // Don't allow dragging below closed position
+            let minY = pos.opened // Don't allow dragging above open position

Apply similar changes to the rest of the method and showMapAction().

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)

632-644: Bug: allLocations() is called with pointOfUseId instead of merchantId (returns wrong counts).

MerchantDAO expects merchantId. This breaks “Show all locations” counts and results.

Apply:

-        print("DEBUG_FETCH_COUNT:   pointOfUseId: '\(merchant.pointOfUseId)'")
+        #if DEBUG
+        print("DEBUG_FETCH_COUNT:   merchantId: '\(merchant.merchant?.merchantId ?? "nil")'")
+        #endif
@@
-        // Use the same call as AllMerchantLocationsDataProvider
-        ExploreDash.shared.allLocations(for: merchant.pointOfUseId, in: finalBounds, userPoint: finalUserPoint) { [weak self] result in
+        // Use the same call as AllMerchantLocationsDataProvider
+        guard let merchantId = self.merchant.merchant?.merchantId, !merchantId.isEmpty else {
+            DSLogger.log("Missing merchantId; cannot fetch locations")
+            self.locationCount = 0
+            self.updateShowAllLocationsButton()
+            return
+        }
+        ExploreDash.shared.allLocations(for: merchantId, in: finalBounds, userPoint: finalUserPoint) { [weak self] result in

Run to confirm no other callsites still pass pointOfUseId:

#!/bin/bash
rg -nP -C2 'allLocations\s*\(\s*for:\s*[^,]+' | rg -nP 'pointOfUseId'

138-166: Gate phone debug prints behind DEBUG (PII leak risk).

Ship builds should not log phone numbers or system state.

   @objc func callAction() {
     guard let phone = merchant.phone, !phone.isEmpty else { return }
-    print("DEBUG: Original phone: \(phone)")
+    #if DEBUG
+    print("DEBUG: Original phone: \(phone)")
+    #endif
@@
-    print("DEBUG: Extracted digits: \(digits)")
+    #if DEBUG
+    print("DEBUG: Extracted digits: \(digits)")
+    #endif
@@
-    print("DEBUG: URL string: \(urlString)")
+    #if DEBUG
+    print("DEBUG: URL string: \(urlString)")
+    #endif
@@
-    guard let url = URL(string: urlString) else {
-        print("DEBUG: Failed to create URL")
+    guard let url = URL(string: urlString) else {
+        #if DEBUG
+        print("DEBUG: Failed to create URL")
+        #endif
         return
     }
@@
-    guard UIApplication.shared.canOpenURL(url) else {
-        print("DEBUG: Cannot open URL - phone not available")
+    guard UIApplication.shared.canOpenURL(url) else {
+        #if DEBUG
+        print("DEBUG: Cannot open URL - phone not available")
+        #endif
         return
     }
 
-    print("DEBUG: Opening URL: \(url)")
+    #if DEBUG
+    print("DEBUG: Opening URL: \(url)")
+    #endif
     UIApplication.shared.open(url, options: [:], completionHandler: { success in
-        print("DEBUG: URL open result: \(success)")
+        #if DEBUG
+        print("DEBUG: URL open result: \(success)")
+        #endif
     })
   }

1149-1164: Gate CTX field dumps and GameStop debug under DEBUG.

These prints leak merchant identifiers and internals in production logs.

-        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") || self.merchant.name.lowercased().contains("buffalo") {
+        #if DEBUG
+        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") || self.merchant.name.lowercased().contains("buffalo") {
@@
-        }
+        }
+        #endif
@@
-            if self.merchant.name.lowercased().contains("gamestop") {
+            #if DEBUG
+            if self.merchant.name.lowercased().contains("gamestop") {
                 print("🎯 GAMESTOP DENOMINATIONS TYPE DEBUG:")
                 print("   Original denominationsType: '\(denominationsType)'")
                 print("   Lowercased denominationsType: '\(lowercasedType)'")
             }
+            #endif
@@
-            case "fixed":
-                if self.merchant.name.lowercased().contains("gamestop") {
-                    print("   → Matched 'fixed' case - returning Fixed amounts")
-                }
+            case "fixed":
                 return NSLocalizedString("Fixed amounts", comment: "DashSpend")
-            case "flexible", "min-max":
-                if self.merchant.name.lowercased().contains("gamestop") {
-                    print("   → Matched 'flexible'/'min-max' case - returning Flexible amounts")
-                }
+            case "flexible", "min-max":
                 return NSLocalizedString("Flexible amounts", comment: "DashSpend")
-            default:
-                if self.merchant.name.lowercased().contains("gamestop") {
-                    print("   → Matched 'default' case - returning Fixed amounts")
-                }
+            default:
                 return NSLocalizedString("Fixed amounts", comment: "DashSpend")

Also applies to: 1170-1192


943-965: Show the U.S.-only notice conditionally.

Do not show this for non‑US merchants/devices.

   private func createCountryNotice() -> UIView {
@@
-        noticeLabel.text = NSLocalizedString("Note: This card works only in the United States.", comment: "DashSpend")
+        noticeLabel.text = NSLocalizedString("Note: This card works only in the United States.", comment: "DashSpend")
@@
         container.addSubview(noticeLabel)
+        // Hide unless US merchant or US device region
+        let isUSMerchant = (merchant.territory == "US")
+        let isUSDevice = (Locale.current.regionCode == "US")
+        noticeLabel.isHidden = !(isUSMerchant || isUSDevice)
🧹 Nitpick comments (12)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (2)

89-89: Default 1‑mile centerRadius: align via a single source of truth

If the 1‑mile zoom is a UX requirement, great—make it a named constant to avoid drift across controllers/flows.

-    var centerRadius: Double = 1 // 1-mile radius as requested
+    var centerRadius: Double = MapDefaults.centerRadiusMiles // 1-mile radius

Outside this hunk:

private enum MapDefaults {
    static let centerRadiusMiles: Double = 1.0
}

109-111: mapBounds currently represents visibleMapRect — optional rename to visibleBounds

Confirmed: ExploreMapView.mapBounds returns .init(rect: mapView.visibleMapRect) and is passed to the delegate; ExplorePointOfUseListViewController sets model.currentMapBounds = bounds in exploreMapView(_:didChangeVisibleBounds:) (ExplorePointOfUseListViewController.swift:593–596). mapBounds(with:) is used for filter-based fixed-radius fetches (ExplorePointOfUseListViewController.swift:776; kDefaultRadius = 32000).

Optional refactor: rename the property mapBounds -> visibleBounds and keep mapBounds(with:) for radius-based data-fetch bounds; update usages in ExploreMapView.swift (property + _mapViewDidChangeVisibleRegion ≈ lines 110–116 and 263–267) and ExplorePointOfUseListViewController.swift (didChangeVisibleBounds and apply(filters:)).

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)

25-25: Comment contradicts behavior; 320 is a fixed value, not “Max 50%”.

Either compute dynamically or clarify the comment.

Apply:

-internal let kDefaultOpenedMapPosition: CGFloat = 320.0 // Max 50% of screen height
+internal let kDefaultOpenedMapPosition: CGFloat = 320.0 // Fixed opened position (pt)

567-570: SwiftLint: vertical parameter alignment.

Align multiline parameters to silence warnings.

Apply:

-            UIView.animate(withDuration: animationDuration, delay: 0, 
-                          usingSpringWithDamping: 0.8, initialSpringVelocity: 0, 
-                          options: .curveEaseOut) {
+            UIView.animate(withDuration: animationDuration,
+                           delay: 0,
+                           usingSpringWithDamping: 0.8,
+                           initialSpringVelocity: 0,
+                           options: .curveEaseOut) {

775-777: Keep current map bounds when applying filters (optional).

Resetting to a radius-based circle can feel jumpy after the user pans/zooms.

Apply:

-        model.currentMapBounds = mapView.mapBounds(with: filters?.currentRadius ?? kDefaultRadius)
+        let preferredBounds = model.currentMapBounds ?? mapView.mapBounds(with: filters?.currentRadius ?? kDefaultRadius)
+        model.currentMapBounds = preferredBounds
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7)

856-858: Avoid subtitle flicker: use the same source (no distance) at creation and refresh.

Currently created with distance then replaced without distance.

-        subLabel = UILabel()
-        subLabel.text = subtitleWithDistance()
+        subLabel = UILabel()
+        subLabel.text = subtitleWithoutDistance()

Also applies to: 1072-1080


681-711: Gate heavy DEBUG_FETCH_ logs in updateShowAllLocationsButton().*

Reduce noise and runtime overhead in release.

-        print("DEBUG_FETCH_RESULTS: updateShowAllLocationsButton called with locationCount: \(locationCount)")
+        #if DEBUG
+        print("DEBUG_FETCH_RESULTS: updateShowAllLocationsButton called with locationCount: \(locationCount)")
+        #endif
@@
-        print("DEBUG_FETCH_RESULTS: Container has \(containerView.arrangedSubviews.count) arranged subviews")
+        #if DEBUG
+        print("DEBUG_FETCH_RESULTS: Container has \(containerView.arrangedSubviews.count) arranged subviews")
+        #endif
@@
-            print("DEBUG_FETCH_RESULTS: Found show all block with \(showAllBlock.subviews.count) subviews")
+            #if DEBUG
+            print("DEBUG_FETCH_RESULTS: Found show all block with \(showAllBlock.subviews.count) subviews")
+            #endif
@@
-                print("DEBUG_FETCH_RESULTS: Checking subview \(index): \(type(of: subview))")
+                #if DEBUG
+                print("DEBUG_FETCH_RESULTS: Checking subview \(index): \(type(of: subview))")
+                #endif
@@
-                    print("DEBUG_FETCH_RESULTS: Updating button title to: \(newTitle)")
+                    #if DEBUG
+                    print("DEBUG_FETCH_RESULTS: Updating button title to: \(newTitle)")
+                    #endif
@@
-            print("DEBUG_FETCH_RESULTS: No show all block found in container")
+            #if DEBUG
+            print("DEBUG_FETCH_RESULTS: No show all block found in container")
+            #endif

570-679: Scope and gate DEBUG_FETCH_ logs and fix log wording.*

The block is very chatty; gate prints and avoid naming specific brands in generic logs.

-        print("DEBUG_FETCH_COUNT: Starting location fetch for merchant: '\(merchant.name)'")
-        print("DEBUG_FETCH_COUNT: pointOfUseId: '\(merchant.pointOfUseId)'")
+        #if DEBUG
+        print("DEBUG_FETCH_COUNT: Starting location fetch for merchant: '\(merchant.name)'")
+        print("DEBUG_FETCH_COUNT: pointOfUseId: '\(merchant.pointOfUseId)'")
@@
-        print("DEBUG_FETCH_COUNT: Using radius: \(filterRadius) meters (\(filterRadius / kMetersToMilesConversion) miles)")
+        print("DEBUG_FETCH_COUNT: Using radius: \(filterRadius) meters (\(filterRadius / kMetersToMilesConversion) miles)")
@@
-        print("DEBUG_FETCH_COUNT: Location manager status:")
+        print("DEBUG_FETCH_COUNT: Location manager status:")
@@
-            print("DEBUG_FETCH_COUNT: Using current map bounds from user interaction")
+            print("DEBUG_FETCH_COUNT: Using current map bounds from user interaction")
@@
-            print("DEBUG_FETCH_COUNT: Using filter radius: \(filterRadius) meters")
+            print("DEBUG_FETCH_COUNT: Using filter radius: \(filterRadius) meters")
@@
-            print("DEBUG_FETCH_COUNT: No bounds or user location available")
+            print("DEBUG_FETCH_COUNT: No bounds or user location available")
@@
-        print("DEBUG_FETCH_COUNT: Final API call parameters:")
+        print("DEBUG_FETCH_COUNT: Final API call parameters:")
         // …
-        print("DEBUG_FETCH_RESULTS: Results for '\(strongSelf.merchant.name)'")
-        print("DEBUG_FETCH_RESULTS: Total GameStop locations: \(paginationResult.items.count)")
-        print("DEBUG_FETCH_RESULTS: Active GameStop locations: \(activeLocations.count)")
+        print("DEBUG_FETCH_RESULTS: Results for '\(strongSelf.merchant.name)'")
+        print("DEBUG_FETCH_RESULTS: Total locations: \(paginationResult.items.count)")
+        print("DEBUG_FETCH_RESULTS: Active locations: \(activeLocations.count)")
@@
-                    print("DEBUG_FETCH_RESULTS: Inactive GameStop locations: \(inactiveLocations.count)")
+                    print("DEBUG_FETCH_RESULTS: Inactive locations: \(inactiveLocations.count)")
@@
-                    print("DEBUG_FETCH_RESULTS: Setting locationCount to \(newCount)")
+                    print("DEBUG_FETCH_RESULTS: Setting locationCount to \(newCount)")
@@
-                    print("DEBUG_FETCH_RESULTS: locationCount after assignment: \(self?.locationCount ?? -1)")
+                    print("DEBUG_FETCH_RESULTS: locationCount after assignment: \(self?.locationCount ?? -1)")
@@
-                    print("DEBUG_FETCH_RESULTS: locationCount after updateShowAllLocationsButton: \(self?.locationCount ?? -1)")
+                    print("DEBUG_FETCH_RESULTS: locationCount after updateShowAllLocationsButton: \(self?.locationCount ?? -1)")
@@
-                print("DEBUG_FETCH_RESULTS: API call failed with error: \(error)")
+                print("DEBUG_FETCH_RESULTS: API call failed with error: \(error)")
+        #endif

1239-1250: Gate grabber gesture debug prints behind DEBUG.

Avoid noisy logs in release.

-        guard let grabberContainer = grabberContainer else {
-            print("DEBUG: grabberContainer is nil!")
+        guard let grabberContainer = grabberContainer else {
+            #if DEBUG
+            print("DEBUG: grabberContainer is nil!")
+            #endif
             return
         }
-        print("DEBUG: Setting up pan gesture on grabber container")
+        #if DEBUG
+        print("DEBUG: Setting up pan gesture on grabber container")
+        #endif
@@
-        print("DEBUG: Pan gesture added to grabber container")
+        #if DEBUG
+        print("DEBUG: Pan gesture added to grabber container")
+        #endif

35-41: Fix access control and closure signatures.

Members are marked public in an internal type; also prefer () -> Void. Make the class public or lower members to internal.

-    public var payWithDashHandler: (()->())?
-    public var sellDashHandler: (()->())?
-    public var dashSpendAuthHandler: (()->())?
-    public var buyGiftCardHandler: (()->())?
-    public var showAllLocationsActionBlock: (() -> ())?
-    public var infoButtonActionBlock: (() -> ())? // For parent view controller to handle info button tap
+    var payWithDashHandler: (() -> Void)?
+    var sellDashHandler: (() -> Void)?
+    var dashSpendAuthHandler: (() -> Void)?
+    var buyGiftCardHandler: (() -> Void)?
+    var showAllLocationsActionBlock: (() -> Void)?
+    var infoButtonActionBlock: (() -> Void)? // For parent view controller to handle info button tap

If these must be public, declare the type public final class PointOfUseDetailsView and keep members public.


909-910: Redundant conditional: same color on both branches.

Simplify.

-        subtitleLabel.textColor = self.merchant.active ? .dw_tertiaryText() : .dw_tertiaryText()
+        subtitleLabel.textColor = .dw_tertiaryText()

1086-1105: Remove unused helper getAddressWithDistance(_:). Only the attributed variant is referenced (DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift:457, 1081); the String-returning helper is defined at the same file (lines 1086–1105) and has no usages — remove it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68d9c7d and d50273e.

📒 Files selected for processing (8)
  • DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (10 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (3 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (5 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is confirmed unused after code analysis. Radius functionality is properly handled through model.currentRadius and filters?.currentRadius, making the controller property redundant and safe to remove.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is unused and should be removed. Radius functionality is handled through the model.currentRadius and filters system instead of a controller-level property.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
🧬 Code graph analysis (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (6)
  • tableView (629-685)
  • tableView (687-703)
  • tableView (709-726)
  • tableView (728-739)
  • tableView (741-763)
  • tableView (765-769)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (3)
  • mapView (240-245)
  • mapView (247-257)
  • mapView (273-279)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/PointOfUseListModel.swift (2)
  • items (57-60)
  • fetch (201-204)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • items (38-39)
  • items (42-175)
  • allLocations (200-247)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift (6)
  • items (24-46)
  • items (70-93)
  • items (112-127)
  • fetch (58-64)
  • fetch (102-106)
  • fetch (136-141)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (1)
  • mapBounds (113-115)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (5)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)
  • callAction (91-106)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • allLocations (200-247)
  • items (38-39)
  • items (42-175)
DashWallet/Sources/Models/Explore Dash/ExploreDash.swift (1)
  • allLocations (189-192)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)
  • items (34-79)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (4)
  • mapView (240-245)
  • mapView (247-257)
  • mapView (273-279)
  • configureHierarchy (204-234)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)
  • configureHierarchy (211-234)
  • callAction (138-166)
  • websiteAction (184-188)
  • setupGrabberPanGesture (1239-1250)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (1)
  • configureHierarchy (77-84)
DashWallet/Sources/Categories/UIHostingController+DashWallet.swift (1)
  • setDetent (21-31)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift

[Warning] 568-568: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 569-569: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 138-138: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 113-113: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 128-128: Unimplemented functions should be marked as unavailable

(unavailable_function)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 213-213: Prefer object literals over image and color inits

(object_literal)


[Warning] 284-284: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 390-390: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 358-358: Prefer object literals over image and color inits

(object_literal)


[Warning] 452-452: Prefer object literals over image and color inits

(object_literal)


[Warning] 518-518: Prefer object literals over image and color inits

(object_literal)


[Warning] 1111-1111: Prefer object literals over image and color inits

(object_literal)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1283-1283: Unimplemented functions should be marked as unavailable

(unavailable_function)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 38-38: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 59-59: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 281-281: Force unwrapping should be avoided

(force_unwrapping)

🔇 Additional comments (7)
DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1)

40-46: Domain updates to wallet.uphold.com look correct.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swift (3)

28-33: Initializer threading filter/map state: LGTM.

Passing currentFilters/currentMapBounds through the VC constructor is clean.


53-59: Hiding segments via zero rows: LGTM.

Keeps layout stable without rendering an unused section.


70-75: Data provider receives snapshot state; ensure provider honors dynamic updates.

Your provider currently prioritizes its captured properties over parameters (see provider comment); fix there to use method params so map pans/filters take effect.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (2)

142-149: Passing filters/map bounds to details VC: LGTM.


593-598: Use of visible map bounds: LGTM.

Switching to real bounds makes list refresh track the map.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)

89-99: Filtering to active locations only: LGTM.

Matches the details count and avoids showing inactive entries.

- Fix force unwrapping crashes in coordinate handling and pagination
- Unify default search radius values across codebase
- Set SVG gift card icons as template images for proper rendering
- Fix generic parameter inference errors in ExploreMapView compactMap
- Remove trailing whitespaces and unused radius property

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

Co-Authored-By: Claude <[email protected]>
Copy link

@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 (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExploreSyncBannerView.swift (1)

48-52: Add a bottom constraint to avoid ambiguous layout and clipping.

The label is only pinned on top/leading/trailing. Without a bottom anchor, the banner’s height can be ambiguous and longer localizations may clip. PointOfUseDetailsView pins both top and bottom; mirror that here.

 NSLayoutConstraint.activate([
   label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
   label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
   label.topAnchor.constraint(equalTo: topAnchor, constant: 8),
+  label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
 ])
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (1)

48-50: Sell tag never shows for .sell ATMs (logic bug)

Current predicates hide both tags for .sell type. Show Sell for .sell and .buySell; hide only for .buy.

-        sellTag.isHidden = atm.type != .buySell
+        sellTag.isHidden = atm.type == .buy
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (1)

140-165: Gate debug prints behind DEBUG flag.

The debug print statements are currently unconditional and will appear in production logs.

Apply this diff to prevent debug output in production:

-        print("DEBUG: Original phone: \(phone)")
+        #if DEBUG
+        print("DEBUG: Original phone: \(phone)")
+        #endif

         // Extract only digits for phone call
         let digits = phone.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
         guard !digits.isEmpty else { return }
-        print("DEBUG: Extracted digits: \(digits)")
+        #if DEBUG
+        print("DEBUG: Extracted digits: \(digits)")
+        #endif

         // Use telprompt: to directly open phone app (tel: shows options)
         let urlString = "telprompt:\(digits)"
-        print("DEBUG: URL string: \(urlString)")
+        #if DEBUG
+        print("DEBUG: URL string: \(urlString)")
+        #endif

         guard let url = URL(string: urlString) else {
-            print("DEBUG: Failed to create URL")
+            #if DEBUG
+            print("DEBUG: Failed to create URL")
+            #endif
             return
         }

         // Check if device can open the URL
         guard UIApplication.shared.canOpenURL(url) else {
-            print("DEBUG: Cannot open URL - phone not available")
+            #if DEBUG
+            print("DEBUG: Cannot open URL - phone not available")
+            #endif
             return
         }

-        print("DEBUG: Opening URL: \(url)")
+        #if DEBUG
+        print("DEBUG: Opening URL: \(url)")
+        #endif
         UIApplication.shared.open(url, options: [:], completionHandler: { success in
-            print("DEBUG: URL open result: \(success)")
+            #if DEBUG
+            print("DEBUG: URL open result: \(success)")
+            #endif
         })
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)

498-507: Use the filtered groups when location is unavailable (bug: local var unused).

You build filterGroups but pass currentSegment.filterGroups to the view, negating the location-based filtering.

Apply this diff:

-        let filterGroups = currentSegment.filterGroups.filter { filter in
+        let filterGroups = currentSegment.filterGroups.filter { filter in
             DWLocationManager.shared.currentLocation != nil || (filter != .sortBy && filter != .radius)
         }
@@
-        let filtersView = MerchantFiltersView(
-            currentFilters: model.filters,
-            filterGroups: currentSegment.filterGroups,
+        let filtersView = MerchantFiltersView(
+            currentFilters: model.filters,
+            filterGroups: filterGroups,
             territoriesDataSource: currentSegment.territoriesDataSource,
             sortOptions: currentSegment.sortOptions
         ) { [weak self] filters in
             self?.apply(filters: filters)
         }

Also applies to: 511-516

♻️ Duplicate comments (10)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)

1149-1163: Gate extensive merchant debug prints behind DEBUG flag.

These debug prints include sensitive merchant information and should not appear in production logs.

+        #if DEBUG
         if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") || self.merchant.name.lowercased().contains("buffalo") {
             print("🎯 MERCHANT DEBUG: \(self.merchant.name)")
             // ... rest of debug prints
         }
+        #endif

1170-1192: Remove GameStop-specific debug logging from production code.

+            #if DEBUG
             if self.merchant.name.lowercased().contains("gamestop") {
                 print("🎯 GAMESTOP DENOMINATIONS TYPE DEBUG:")
                 // ... debug prints
             }
+            #endif

Also remove the debug prints from inside the switch cases.


641-643: Critical: Use merchantId instead of pointOfUseId for location fetch.

The allLocations API expects a merchantId but is being passed a pointOfUseId, which causes incorrect location counts.

-        ExploreDash.shared.allLocations(for: merchant.pointOfUseId, in: finalBounds, userPoint: finalUserPoint) { [weak self] result in
+        guard let merchantId = self.merchant.merchant?.merchantId, !merchantId.isEmpty else {
+            DSLogger.log("Missing merchantId; cannot fetch locations")
+            self.locationCount = 0
+            self.updateShowAllLocationsButton()
+            return
+        }
+        ExploreDash.shared.allLocations(for: merchantId, in: finalBounds, userPoint: finalUserPoint) { [weak self] result in
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)

427-432: Gate CTX merchant debug logging.

+                #if DEBUG
                 if pointOfUse.name.lowercased().contains("buffalo") || pointOfUse.name.lowercased().contains("gamestop") {
                     print("🎯 CTX MERCHANT INFO DEBUG: \(pointOfUse.name)")
                     // ... debug prints
                 }
+                #endif

38-38: Remove unused IUO property to prevent crashes.

The showMapButton property is declared but never properly initialized, which can lead to runtime crashes if accessed.

     private var contentViewTopConstraint: NSLayoutConstraint?
-    private var showMapButton: UIButton!

40-40: Fix access level mismatch.

The initializer is public while the class is internal, causing a compilation error.

-    public init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, currentFilters: PointOfUseListFilters? = nil, currentMapBounds: ExploreMapBounds? = nil) {
+    init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, currentFilters: PointOfUseListFilters? = nil, currentMapBounds: ExploreMapBounds? = nil) {

283-283: Avoid force unwrapping to prevent crashes.

Force unwrapping contentViewTopConstraint can cause crashes if it's nil.

+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            contentViewTopConstraint = topConstraint
             constraint = [
-                contentViewTopConstraint!,
+                topConstraint,
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (3)

24-31: Consider removing captured currentFilters/currentMapBounds to avoid stale results.

These captured properties override the live inputs from the model, causing the view not to update when the user pans the map or changes filters.

 class AllMerchantLocationsDataProvider: PointOfUseDataProvider {
     private let pointOfUse: ExplorePointOfUse
-    private let currentFilters: PointOfUseListFilters?
-    private let currentMapBounds: ExploreMapBounds?

-    init(pointOfUse: ExplorePointOfUse, currentFilters: PointOfUseListFilters? = nil, currentMapBounds: ExploreMapBounds? = nil) {
+    init(pointOfUse: ExplorePointOfUse) {
         self.pointOfUse = pointOfUse
-        self.currentFilters = currentFilters
-        self.currentMapBounds = currentMapBounds
         super.init()
     }

40-65: Honor method-provided bounds/filters instead of overriding with captured values.

The logic should prefer explicit bounds from the method parameters.

Use the incoming bounds parameter if non-nil, otherwise fall back to deriving bounds from user location and filters. Don't override with captured values.


67-70: Cache comparison should include userPoint.

Results ordering depends on userPoint, so the cache can be stale after the user moves.

-        if lastQuery == query && !items.isEmpty && lastBounds == finalBounds {
+        if lastQuery == query && !items.isEmpty && lastBounds == finalBounds && 
+           lastUserPoint?.latitude == finalUserPoint?.latitude && 
+           lastUserPoint?.longitude == finalUserPoint?.longitude {
             completion(.success(items))
             return
         }
🧹 Nitpick comments (14)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExploreSyncBannerView.swift (3)

23-31: Allow wrapping and support Dynamic Type.

Hard-coding to one line risks truncation in some locales. Enable wrapping and ensure scaling.

         label.translatesAutoresizingMaskIntoConstraints = false
         label.text = NSLocalizedString("Sync in progress… Results may not be complete.", comment: "Explore Dash")
         label.textColor = .white
         label.font = .dw_font(forTextStyle: .footnote)
         label.textAlignment = .center
-        label.numberOfLines = 1
+        label.numberOfLines = 0
+        label.lineBreakMode = .byWordWrapping
+        label.adjustsFontForContentSizeCategory = true
         return label

21-21: Fix SwiftLint warning: remove empty line after opening brace.

Matches vertical_whitespace_opening_braces rule.

-class ExploreSyncBannerView: UIView {
-
+class ExploreSyncBannerView: UIView {

25-25: Clarify the translator comment for better localization context.

Give translators precise UI context.

-        label.text = NSLocalizedString("Sync in progress… Results may not be complete.", comment: "Explore Dash")
+        label.text = NSLocalizedString("Sync in progress… Results may not be complete.", comment: "Explore screen sync banner")
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (2)

35-46: Good nil-safety and auth gating; refine text join and distance rounding

Nice switch to optional binding and auth gating. Two small fixes:

  • Avoid a dangling " • " when source is nil/empty.
  • Let the formatter handle rounding; floor(distance) can under-report.

Apply:

-            let distanceText: String = ExploreDash.distanceFormatter
-                .string(from: Measurement(value: floor(distance), unit: UnitLength.meters))
-            subLabel.text = "\(distanceText) • \(pointOfUse.source ?? "")"
+            let distanceText = ExploreDash.distanceFormatter
+                .string(from: Measurement(value: distance, unit: .meters))
+            if let source = pointOfUse.source?.trimmingCharacters(in: .whitespacesAndNewlines),
+               !source.isEmpty {
+                subLabel.text = "\(distanceText) • \(source)"
+            } else {
+                subLabel.text = distanceText
+            }

118-123: Remove redundant centerX constraint to prevent over-constraint risk

Leading+trailing already center the label horizontally. Drop centerXAnchor for cleaner constraints.

         NSLayoutConstraint.activate([
             label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6),
             label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6),
-            label.centerXAnchor.constraint(equalTo: centerXAnchor),
             label.centerYAnchor.constraint(equalTo: centerYAnchor),
         ])
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift (1)

96-99: Introduce a typed Error for missing bounds (preferred) — or at minimum fix the NSError domain

Guard failure should return a typed error instead of an ad‑hoc NSError (or at least correct the domain). The single occurrence is at DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift:97.

Option A (preferred — add a typed error):

-        guard let lastBounds = lastBounds else {
-            completion(.failure(NSError(domain: "MerchantsDataProvider", code: -1, userInfo: [NSLocalizedDescriptionKey: "No bounds available for pagination"])))
+        guard let lastBounds = lastBounds else {
+            completion(.failure(MerchantsDataProviderError.missingBounds))
             return
         }

Add once in this file (top-level, near the providers):

enum MerchantsDataProviderError: LocalizedError {
    case missingBounds
    var errorDescription: String? {
        switch self {
        case .missingBounds: return NSLocalizedString("No bounds available for pagination", comment: "Pagination error")
        }
    }
}

Option B (minimal — fix domain and message localization):

-            completion(.failure(NSError(domain: "MerchantsDataProvider", code: -1, userInfo: [NSLocalizedDescriptionKey: "No bounds available for pagination"])))
+            completion(.failure(NSError(domain: "NearbyMerchantsDataProvider",
+                                        code: 1,
+                                        userInfo: [NSLocalizedDescriptionKey:
+                                                   NSLocalizedString("No bounds available for pagination", comment: "Pagination error")])))
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2)

26-26: Avoid implicitly unwrapped optional to prevent potential crashes.

The distanceLabel is declared as an IUO but is properly initialized in configureHierarchy(). Consider making it non-optional with lazy initialization or a regular optional.

-    private var distanceLabel: UILabel!
+    private lazy var distanceLabel: UILabel = {
+        let label = UILabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.font = .systemFont(ofSize: 12, weight: .regular)
+        label.textColor = .dw_tertiaryText()
+        label.isHidden = true
+        label.textAlignment = .right
+        return label
+    }()

Then update configureHierarchy() to just add it as a subview without initializing:

-        distanceLabel = UILabel()
-        distanceLabel.translatesAutoresizingMaskIntoConstraints = false
-        distanceLabel.font = .systemFont(ofSize: 12, weight: .regular)
-        distanceLabel.textColor = .dw_tertiaryText()
-        distanceLabel.isHidden = true
-        distanceLabel.textAlignment = .right
         mainStackView.addArrangedSubview(distanceLabel)

51-51: Remove trailing semicolon.

-        let paymentIconName = isGiftCard ? "image.explore.dash.wts.payment.gift-card" : "image.explore.dash.wts.payment.dash";
+        let paymentIconName = isGiftCard ? "image.explore.dash.wts.payment.gift-card" : "image.explore.dash.wts.payment.dash"
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)

110-178: Gate debug prints in pan gesture handler.

The pan gesture handler contains debug prints that should not appear in production.

-        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #if DEBUG
+        print("DEBUG: Pan gesture triggered - state: \(sender.state.rawValue)")
+        #endif
         guard let contentViewTopConstraint = contentViewTopConstraint else {
-            print("DEBUG: contentViewTopConstraint is nil!")
+            #if DEBUG
+            print("DEBUG: contentViewTopConstraint is nil!")
+            #endif
             return
         }
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (5)

25-25: Comment vs behavior mismatch (“Max 50% of screen height”).

Constant is 320pt; logic above clamps at a dynamic 50%. Either compute dynamically everywhere (preferred) or correct the comment.

Do you want me to refactor all call sites to use a computed openY so the comment becomes true?


394-401: Sync banner overlay is fine; consider safe-area and hit-testing.

Looks good. Ensure the banner doesn’t block interactions unintentionally when height=0 (it’s hidden post-animation).

Optionally set isUserInteractionEnabled = false when hidden, and accessibilityTraits = .updatesFrequently.

Also applies to: 462-474


257-257: SwiftLint: put attributes on their own line.

Fix rule “attributes”: @objc should be on its own line for functions.

-    @objc private func databaseHasBeenUpdated() {
+    @objc
+    private func databaseHasBeenUpdated() {

If desired, I can sweep the file for similar cases.


775-779: Map-bounds init on filter apply—add permission guard.

If location is denied, deriving bounds from map radius may be misleading versus showing global results. Consider clearing bounds when permission is denied.

Apply this diff:

-        model.currentMapBounds = mapView.mapBounds(with: filters?.currentRadius ?? kDefaultRadius)
+        if DWLocationManager.shared.isPermissionDenied || DWLocationManager.shared.needsAuthorization {
+            model.currentMapBounds = nil
+        } else {
+            model.currentMapBounds = mapView.mapBounds(with: filters?.currentRadius ?? kDefaultRadius)
+        }

566-569: Align multiline call parameters (SwiftLint).

Parameters in the animated block should be vertically aligned.

-            UIView.animate(withDuration: animationDuration, delay: 0,
-                          usingSpringWithDamping: 0.8, initialSpringVelocity: 0,
-                          options: .curveEaseOut) {
+            UIView.animate(withDuration: animationDuration,
+                           delay: 0,
+                           usingSpringWithDamping: 0.8,
+                           initialSpringVelocity: 0,
+                           options: .curveEaseOut) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d50273e and 03fad5b.

📒 Files selected for processing (17)
  • DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (12 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Info/MerchantTypesDialog.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (3 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (12 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExploreSyncBannerView.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/MerchantFiltersView.swift (16 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/MerchantFiltersViewModel.swift (13 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/PointOfUseListFiltersModel.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/TerritoryPickerView.swift (5 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (5 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (2 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (4 hunks)
✅ Files skipped from review due to trivial changes (4)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Info/MerchantTypesDialog.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/TerritoryPickerView.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/MerchantFiltersView.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/PointOfUseListFiltersModel.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/AtmDetailsView.swift
  • DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is confirmed unused after code analysis. Radius functionality is properly handled through model.currentRadius and filters?.currentRadius, making the controller property redundant and safe to remove.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/MerchantFiltersViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is unused and should be removed. Radius functionality is handled through the model.currentRadius and filters system instead of a controller-level property.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/MerchantFiltersViewModel.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
🧬 Code graph analysis (9)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)
  • fetch (87-100)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AtmDataProvider.swift (1)
  • fetch (65-70)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (2)
  • setupInfoButton (73-77)
  • infoButtonAction (79-84)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift (6)
  • items (24-46)
  • items (70-93)
  • items (116-131)
  • fetch (58-64)
  • fetch (106-110)
  • fetch (140-145)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • items (38-39)
  • items (42-175)
  • allLocations (200-247)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AtmDataProvider.swift (2)
  • items (26-56)
  • fetch (65-70)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (1)
  • mapBounds (113-115)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (5)
DashWallet/Sources/Models/Explore Dash/Infrastructure/Database Connection/ExploreDatabaseConnection.swift (1)
  • hasOldMerchantIdSchema (43-60)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)
  • items (34-79)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift (3)
  • items (24-46)
  • items (70-93)
  • items (116-131)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/PointOfUseListModel.swift (1)
  • items (57-60)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AtmDataProvider.swift (1)
  • items (26-56)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExploreSyncBannerView.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)
  • setupView (1286-1299)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (6)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (1)
  • callAction (91-106)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (2)
  • stopNetworkMonitoring (52-54)
  • startNetworkMonitoring (36-50)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • allLocations (200-247)
  • items (38-39)
  • items (42-175)
DashWallet/Sources/Models/Explore Dash/ExploreDash.swift (1)
  • allLocations (189-192)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)
  • items (34-79)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (2)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/AtmItemCell.swift (1)
  • update (28-50)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swift (1)
  • update (41-49)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/MerchantFiltersViewModel.swift (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/PointOfUseListModel.swift (1)
  • resetFilters (186-189)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (5)
  • mapView (250-255)
  • mapView (257-267)
  • mapView (283-289)
  • configureHierarchy (214-244)
  • show (139-180)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (2)
  • configureHierarchy (336-492)
  • show (142-148)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (2)
  • configureHierarchy (232-238)
  • show (190-197)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (4)
  • configureHierarchy (211-234)
  • callAction (138-166)
  • websiteAction (184-188)
  • setupGrabberPanGesture (1238-1249)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift

[Warning] 257-257: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 567-567: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 568-568: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExploreSyncBannerView.swift

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

(vertical_whitespace_opening_braces)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 138-138: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 113-113: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 128-128: Unimplemented functions should be marked as unavailable

(unavailable_function)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 213-213: Prefer object literals over image and color inits

(object_literal)


[Warning] 284-284: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 390-390: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 358-358: Prefer object literals over image and color inits

(object_literal)


[Warning] 452-452: Prefer object literals over image and color inits

(object_literal)


[Warning] 518-518: Prefer object literals over image and color inits

(object_literal)


[Warning] 1110-1110: Prefer object literals over image and color inits

(object_literal)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1282-1282: Unimplemented functions should be marked as unavailable

(unavailable_function)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift

[Warning] 26-26: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Lines should not have trailing semicolons

(trailing_semicolon)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/MerchantFiltersViewModel.swift

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

(vertical_whitespace_opening_braces)


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

(cyclomatic_complexity)

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 38-38: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 59-59: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 283-283: Force unwrapping should be avoided

(force_unwrapping)

🔇 Additional comments (7)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift (1)

96-104: Make nextPage() return empty .success([]) when bounds/authorization are missing (mirror items())

items(...) currently returns .success([]) when bounds/authorization are unavailable; nextPage(...) returns an error and can surface an unnecessary error UI. Short‑circuit with an empty success and reset pagination.

-        guard let lastBounds = lastBounds else {
-            completion(.failure(NSError(domain: "MerchantsDataProvider", code: -1, userInfo: [NSLocalizedDescriptionKey: "No bounds available for pagination"])))
-            return
-        }
-        fetch(by: lastQuery, in: lastBounds, userPoint: lastUserPoint, with: lastFilters,
+        guard let lastBounds = lastBounds,
+              let lastUserPoint = lastUserPoint,
+              DWLocationManager.shared.isAuthorized else {
+            // Mirror `items(...)` fallback: clear state and return empty list.
+            items = []
+            currentPage = nil
+            completion(.success(items))
+            return
+        }
+        fetch(by: lastQuery, in: lastBounds, userPoint: lastUserPoint, with: lastFilters,
               offset: nextOffset) { [weak self] result in
             self?.handle(result: result, appending: true, completion: completion)
         }
  • Search found other pagination overrides; make this behavior consistent:
    • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift:81
    • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AtmDataProvider.swift:58
    • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/MerchantsDataProvider.swift:48, 95, 133
  • Confirm this change does not conflict with the existing permission‑denied branch in MerchantsDataProvider.swift that currently calls fetch(by:..., in: nil, ...) (adjust as needed).
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Filters/Model/MerchantFiltersViewModel.swift (1)

55-55: Good addition for location-aware features.

The isLocationServiceEnabled property properly tracks location authorization status for the filter view model.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift (2)

110-111: Good typo fix!

Fixed "redius" → "radius" in the localized string.


193-193: Properly passes filter context to details view.

The addition of currentFilters and currentMapBounds parameters ensures the detail view has proper context for location-based features.

DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (3)

220-233: DB sync gating logic LGTM.

Good fallback to show banner on errors or old schema.

Confirm kExploreDashDatabaseName path resolves under Documents across app targets (App/Extension).


592-597: Switch to visible-bounds-driven refresh looks correct.

Updating model.currentMapBounds on map changes is aligned with the data providers’ new behavior.

Watch for excessive refresh calls while panning; if needed, debounce in ExploreMapViewDelegate.


143-147: Approve — initializer usages updated and consistent
Both call sites pass currentFilters and currentMapBounds (MerchantListViewController.swift:193; ExplorePointOfUseListViewController.swift:143). Nice wiring — keeps details/all-locations contexts consistent with list/map.

- Add CLAUDE.md with project overview, build commands, and safety guidelines
- Add ios-swift-safety-patterns.md with crash prevention patterns
- Remove CLAUDE.md from gitignore to enable team knowledge sharing
- Document force unwrapping fixes, map filtering architecture, and common build failures
- Include code review checklist and safety patterns from recent debugging session

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

Co-Authored-By: Claude <[email protected]>
Copy link

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

🧹 Nitpick comments (6)
.claude/agents/CLAUDE.md (4)

172-176: Add a fenced-code language to satisfy markdownlint (MD040).

The triple backticks around the sibling directories block lack a language hint.

Apply:

-```
+```text
 ../DashSync/          # Core Dash protocol library
 ../dapi-grpc/         # gRPC API definitions  
 ../dashwallet-ios/    # This repository

---

`30-31`: **Fix unintended Markdown heading; use a bullet or sentence.**

This line renders as an H1 due to the leading "#".


```diff
-# Or in Xcode, run tests on iPhone 8 simulator (as configured in Fastfile)
+- Or in Xcode, run tests on the configured simulator (e.g., iPhone 16 Pro) as defined in Fastfile.

186-191: Expand “Security” with explicit secrets management guidance.

Given the PR comment about exposed Uphold secrets, document hard guardrails here.

 ### Security
 - Jailbreak detection
 - Hardware encryption usage
 - Private key protection
 - Secure enclave integration
+ - Secrets management
+   - Never commit API keys, tokens, or client secrets. Store in CI/CD secrets and local `.xcconfig` (untracked).
+   - Use environment-specific `.xcconfig` with `$(UPHOLD_API_KEY)`-style substitutions.
+   - Enable pre-commit/CI scanners (e.g., `git-secrets` or `trufflehog`) and block on findings.
+   - Rotate any credentials that ever appeared in Git history; treat them as compromised.

I can add a short “Secrets Setup” snippet for Fastlane/Xcode if you’d like.


230-230: Add newline at EOF.

Tiny formatting nit; helps various tools.

.claude/agents/ios-swift-safety-patterns.md (2)

121-125: Reconsider treating 0,0 as invalid coordinates.

Excluding (0,0) prevents showing legitimate points near the Gulf of Guinea. If (0,0) is only a sentinel in your data, gate on upstream source flags instead of blanket-filtering.

Possible alternative:

var isValid: Bool {
    CLLocationCoordinate2DIsValid(self)
    // Optional: treat (0,0) as invalid only if your API marks coords as missing/derived.
}

177-189: Prefer Unified Logging over print for production diagnostics.

Use os.Logger/OSLog for structured logs and privacy controls.

-        } else {
-            print("⚠️ Could not load template image: \(imageName)")
-        }
+        } else {
+            if #available(iOS 14.0, *) {
+                let logger = Logger(subsystem: "org.dash.wallet", category: "UIAssets")
+                logger.warning("Could not load template image: \(imageName, privacy: .public)")
+            } else {
+                NSLog("⚠️ Could not load template image: %@", imageName)
+            }
+        }

(Remember to import OSLog for iOS 14+.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03fad5b and 14242e7.

📒 Files selected for processing (3)
  • .claude/agents/CLAUDE.md (1 hunks)
  • .claude/agents/ios-swift-safety-patterns.md (1 hunks)
  • .gitignore (0 hunks)
💤 Files with no reviewable changes (1)
  • .gitignore
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-05T04:46:12.717Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#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/agents/CLAUDE.md
🪛 markdownlint-cli2 (0.17.2)
.claude/agents/CLAUDE.md

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

(MD040, fenced-code-language)

🔇 Additional comments (4)
.claude/agents/CLAUDE.md (2)

1-230: Docs quality: solid overview and actionable checklists.

Good structure, practical tips, and repo-specific context. Only minor nits above.


159-163: Update .claude/agents/CLAUDE.md — bump minimum iOS to 15.0 and require an Xcode that provides the iOS 15 SDK.

DashWallet.xcodeproj/project.pbxproj sets IPHONEOS_DEPLOYMENT_TARGET = 15.0 and SWIFT_VERSION = 5.0; change the docs (lines 159–163) to reflect iOS 15.0+ and verify CI images include the matching Xcode/iOS SDK.

.claude/agents/ios-swift-safety-patterns.md (2)

227-235: Nice: build validation snippet is concrete and up to date.

Destination uses a current simulator; complements earlier guidance.


1-387: Overall: excellent, pragmatic safety guide.

Strong, repo-aware examples across optionals, MapKit safety, and UI state. Minimal tweaks above.

Copy link
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.

Changes to consider:

  1. remove DEBUG things for the logs
  2. remove the CTX logout API, which does not exist, unless it actually does exist
  3. Xcode 16 is required in the agent

- Remove all debug print statements that were causing build noise
- Restore proper file structure after correcting compilation errors
- Clean up phone call action debug logging
- Clean up location fetch and merchant data debug logging

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

Co-Authored-By: Claude <[email protected]>
Copy link

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

♻️ Duplicate comments (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

612-614: Bug: pass merchantId (not pointOfUseId) to allLocations — fix both callsites

ExploreDash.allLocations / MerchantDAO.allLocations expect a merchantId; currently two callsites pass pointOfUseId which produces wrong counts and inconsistent “Show all locations” titles.

--- a/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift
+++ b/DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift
@@
-        // Use the same call as AllMerchantLocationsDataProvider
-        ExploreDash.shared.allLocations(for: merchant.pointOfUseId, in: finalBounds, userPoint: finalUserPoint) { [weak self] result in
+        // Use merchantId as required by DAO
+        guard let merchantId = self.merchant.merchant?.merchantId, !merchantId.isEmpty else {
+            DSLogger.log("Missing merchantId; cannot fetch locations")
+            self.locationCount = 0
+            self.updateShowAllLocationsButton()
+            return
+        }
+        ExploreDash.shared.allLocations(for: merchantId, in: finalBounds, userPoint: finalUserPoint) { [weak self] result in
  • Also update DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (line ~89): replace
    dataSource.allLocations(for: pointOfUse.pointOfUseId, ...) with a call that passes the merchant's merchantId (e.g. pointOfUse.merchant?.merchantId or pointOfUse.merchantId). Add a guard + log and return an empty PaginationResult on missing id to preserve failure handling.
🧹 Nitpick comments (9)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (9)

177-181: Normalize website URL; guard canOpenURL

Handle missing scheme and avoid opening invalid URLs.

-    guard let website = merchant.website, let url = URL(string: website) else { return }
-    UIApplication.shared.open(url, options: [:], completionHandler: nil)
+    guard let website = merchant.website else { return }
+    let normalized: String
+    if website.hasPrefix("http://") || website.hasPrefix("https://") {
+        normalized = website
+    } else {
+        normalized = "https://" + website
+    }
+    guard let url = URL(string: normalized), UIApplication.shared.canOpenURL(url) else { return }
+    UIApplication.shared.open(url, options: [:], completionHandler: nil)

811-817: Initialize subtitle without distance to match design and avoid flicker

You immediately replace it in refreshSubtitle(); set the desired value upfront.

-        subLabel.text = subtitleWithDistance()
+        subLabel.text = subtitleWithoutDistance()

988-989: Localize user-facing merchant type strings

Hardcoded strings break i18n.

-                return m.type == .onlineAndPhysical ? "Physical Merchant, Online" : "Physical Merchant"
+                return m.type == .onlineAndPhysical
+                    ? NSLocalizedString("Physical Merchant, Online", comment: "Merchant type")
+                    : NSLocalizedString("Physical Merchant", comment: "Merchant type")

1089-1091: Localize “away”

Use a format string for translation and reordering.

-            let distanceString = NSAttributedString(string: "\n\(distanceText) away", attributes: distanceAttributes)
+            let awayText = String.localizedStringWithFormat(
+                NSLocalizedString("%@ away", comment: "Distance away"),
+                distanceText
+            )
+            let distanceString = NSAttributedString(string: "\n\(awayText)", attributes: distanceAttributes)

1104-1106: Remove empty debug conditionals

These no-op if-blocks add noise and confuse readers.

-        if self.merchant.name.lowercased().contains("gamestop") || self.merchant.name.lowercased().contains("spotify") || self.merchant.name.lowercased().contains("buffalo") {
-        }
...
-            if self.merchant.name.lowercased().contains("gamestop") {
-            }
...
-                if self.merchant.name.lowercased().contains("gamestop") {
-                }
...
-                if self.merchant.name.lowercased().contains("gamestop") {
-                }
...
-                if self.merchant.name.lowercased().contains("gamestop") {
-                }

Also applies to: 1112-1115, 1118-1120, 1122-1124, 1126-1128


864-864: Simplify textColor assignment

Both branches set the same color.

-        subtitleLabel.textColor = self.merchant.active ? .dw_tertiaryText() : .dw_tertiaryText()
+        subtitleLabel.textColor = .dw_tertiaryText()

157-158: Remove no-op completion handler

Silences SwiftLint “unused parameter” and reduces clutter.

-        UIApplication.shared.open(url, options: [:], completionHandler: { success in
-        })
+        UIApplication.shared.open(url, options: [:], completionHandler: nil)

898-920: Show the U.S.-only notice conditionally

Avoid confusing non-U.S. users and non-U.S. merchants.

     private func createCountryNotice() -> UIView {
         let container = UIView()
         container.translatesAutoresizingMaskIntoConstraints = false

         let noticeLabel = UILabel()
         noticeLabel.translatesAutoresizingMaskIntoConstraints = false
         noticeLabel.text = NSLocalizedString("Note: This card works only in the United States.", comment: "DashSpend")
         noticeLabel.font = .dw_font(forTextStyle: .footnote)
         noticeLabel.textColor = .dw_secondaryText()
         noticeLabel.numberOfLines = 0
         noticeLabel.textAlignment = .center
+
+        // Gate visibility by merchant territory or device region
+        let isUSMerchant = (merchant.territory == "US")
+        let isUSDevice = (Locale.current.regionCode == "US")
+        container.isHidden = !(isUSMerchant || isUSDevice)

         container.addSubview(noticeLabel)

651-665: Hold a reference to the “Show all locations” button (avoid brittle view crawling)

Relying on “last arranged subview” is fragile. Keep a weak reference and update directly.

@@
-    private var locationCount: Int = 1
+    private var locationCount: Int = 1
+    private weak var showAllLocationsButton: UIButton?
@@
-        if let showAllBlock = containerView.arrangedSubviews.last {
-            // Find the button within the block
-            for (index, subview) in showAllBlock.subviews.enumerated() {
-                if let button = subview as? UIButton {
-                    let newTitle = String.localizedStringWithFormat(
-                        NSLocalizedString("Show all locations (%d)", comment: "Show all locations with count"),
-                        locationCount
-                    )
-                    button.setTitle(newTitle, for: .normal)
-                    break
-                }
-            }
-        } else {
-        }
+        if let button = showAllLocationsButton {
+            let newTitle = String.localizedStringWithFormat(
+                NSLocalizedString("Show all locations (%d)", comment: "Show all locations with count"),
+                locationCount
+            )
+            button.setTitle(newTitle, for: .normal)
+        }
@@
-        let showAllButton = UIButton()
+        let showAllButton = UIButton()
         showAllButton.translatesAutoresizingMaskIntoConstraints = false
@@
         showAllButton.addTarget(self, action: #selector(showAllLocationsAction), for: .touchUpInside)
+        self.showAllLocationsButton = showAllButton

Also applies to: 678-685, 57-58

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14242e7 and cba31c3.

📒 Files selected for processing (1)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (7)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swift (1)
  • configureHierarchy (64-93)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)
  • configureHierarchy (343-347)
  • callAction (91-106)
  • websiteAction (180-194)
DashWallet/Sources/Models/Explore Dash/Services/DWLocationManager.swift (3)
  • remove (103-106)
  • add (96-101)
  • requestAuthorization (91-94)
DashWallet/Sources/UI/Views/BaseController/BaseViewController+NetworkReachability.swift (2)
  • stopNetworkMonitoring (52-54)
  • startNetworkMonitoring (36-50)
DashWallet/Sources/Models/Explore Dash/Infrastructure/DAO Impl/MerchantDAO.swift (3)
  • allLocations (200-247)
  • items (38-39)
  • items (42-175)
DashWallet/Sources/Models/Explore Dash/ExploreDash.swift (1)
  • allLocations (189-192)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift (1)
  • items (34-79)
🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift

[Warning] 138-138: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 29-29: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 33-33: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 42-42: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 43-43: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 45-45: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 47-47: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 48-48: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 49-49: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 50-50: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 51-51: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 35-35: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 36-36: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 37-37: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 38-38: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 39-39: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 113-113: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 35-35: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 36-36: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 37-37: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 38-38: Return arrow and return type should be separated by a single space or on a separate line

(return_arrow_whitespace)


[Warning] 128-128: Unimplemented functions should be marked as unavailable

(unavailable_function)


[Warning] 157-157: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)


[Warning] 32-32: Prefer -> Void over -> ()

(void_return)


[Warning] 35-35: Prefer -> Void over -> ()

(void_return)


[Warning] 36-36: Prefer -> Void over -> ()

(void_return)


[Warning] 37-37: Prefer -> Void over -> ()

(void_return)


[Warning] 38-38: Prefer -> Void over -> ()

(void_return)


[Warning] 39-39: Prefer -> Void over -> ()

(void_return)


[Warning] 40-40: Prefer -> Void over -> ()

(void_return)


[Warning] 206-206: Prefer object literals over image and color inits

(object_literal)


[Warning] 277-277: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 383-383: Attributes should be on their own lines in functions and types, but on the same line as variables and imports

(attributes)


[Warning] 351-351: Prefer object literals over image and color inits

(object_literal)


[Warning] 445-445: Prefer object literals over image and color inits

(object_literal)


[Warning] 511-511: Prefer object literals over image and color inits

(object_literal)


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

(vertical_whitespace_opening_braces)


[Warning] 1066-1066: Prefer object literals over image and color inits

(object_literal)


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

(vertical_whitespace_closing_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


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

(vertical_whitespace_opening_braces)


[Warning] 1216-1216: Unimplemented functions should be marked as unavailable

(unavailable_function)

🔇 Additional comments (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (1)

1041-1060: Verify usage of getAddressWithDistance(_:); remove or consolidate with attributed variant

ripgrep returned "No files were searched", so callers couldn't be confirmed. Verify whether getAddressWithDistance(_:) is referenced; if unused, delete it to reduce surface area; if used, route callers to the attributed-string variant to keep a single source of truth.

Location: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (lines 1041–1060)

- Remove non-existent CTX logout API call from CTXSpendService
- Clean up remaining debug print statements in PointOfUseDetailsViewController
- Fix trailing whitespace in DWUpholdMainnetConstants
- Update Xcode version requirement to 16+ in documentation

The CTX logout endpoint does not exist, so only clear local tokens.

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

Co-Authored-By: Claude <[email protected]>
Copy link

@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/Models/Explore Dash/Services/CTXSpendService.swift (1)

111-115: DRY token clearing.

clearTokensOnRefreshFailure() and logout() duplicate logic. Factor a single helper to avoid drift.

Apply:

+    @MainActor
+    private func clearAllCredentials() {
+        KeychainService.delete(key: Keys.accessToken)
+        KeychainService.delete(key: Keys.refreshToken)
+        KeychainService.delete(key: Keys.email)
+        userDefaults.removeObject(forKey: Keys.deviceUUID)
+        updateSignInState()
+    }
@@
-    func clearTokensOnRefreshFailure() {
-        KeychainService.delete(key: Keys.accessToken)
-        KeychainService.delete(key: Keys.refreshToken)
-        updateSignInState()
-    }
+    func clearTokensOnRefreshFailure() { Task { @MainActor in clearAllCredentials() } }

And call clearAllCredentials() from logout().

♻️ Duplicate comments (3)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (3)

30-33: Access-level mismatch (compile error).

public member/initializer inside an internal class. Lower access.

Apply:

-    @objc public var payWithDashHandler: (()->())?
+    @objc var payWithDashHandler: (()->())?
@@
-    public init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, currentFilters: PointOfUseListFilters? = nil, currentMapBounds: ExploreMapBounds? = nil) {
+    init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, currentFilters: PointOfUseListFilters? = nil, currentMapBounds: ExploreMapBounds? = nil) {

Also applies to: 40-45


37-39: Remove reintroduced Map button (dead code + IUO crash risk).

showMapButton: UIButton! is never initialized (setup is not called). Accessing it in updateMapButtonVisibility() will crash.

Apply:

-    private var contentViewTopConstraint: NSLayoutConstraint?
-    private var showMapButton: UIButton!
+    private var contentViewTopConstraint: NSLayoutConstraint?
@@
-    private func setupMapButton() { ... }
-
-    @objc
-    private func showMapAction() { ... }
-
-    private func updateMapButtonVisibility() { ... }
+    // Map button removed (unused)

If you keep it, make showMapButton optional and guard before use, but removal is preferable.

Also applies to: 194-231, 233-239


274-282: Avoid force unwrap of contentViewTopConstraint.

Create the constraint, assign to the property, and use the local variable.

Apply:

-            let screenHeight = UIScreen.main.bounds.height
-            let kDefaultBottomHalfPosition = screenHeight * 0.35 // Position higher to show more content including button
-            contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            let screenHeight = UIScreen.main.bounds.height
+            let kDefaultBottomHalfPosition = screenHeight * 0.35 // Position higher to show more content including button
+            let topConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: kDefaultBottomHalfPosition)
+            contentViewTopConstraint = topConstraint
@@
-                contentViewTopConstraint!,
+                topConstraint,
🧹 Nitpick comments (7)
.claude/agents/CLAUDE.md (2)

172-176: Add language to fenced code block (markdownlint MD040).

Specify a language for this fence to satisfy markdownlint and improve rendering.

-```
+```text
 ../DashSync/          # Core Dash protocol library
 ../dapi-grpc/         # gRPC API definitions  
 ../dashwallet-ios/    # This repository
-```
+```

186-191: Strengthen Security section with secrets management guidance.

Given recent concerns about exposed secrets, add explicit guidance on secret storage, rotation, and scanning.

 ### Security
 - Jailbreak detection
 - Hardware encryption usage
 - Private key protection
 - Secure enclave integration
+
+#### Secrets management
+- Never commit API keys, tokens, or credentials. Store them in per-target .xcconfig files or CI secrets.
+- Use environment variables/CI secure vars to inject secrets at build time.
+- Rotate credentials regularly and on every suspected exposure.
+- Add pre-commit hooks and CI checks (e.g., gitleaks) to scan for secrets.
+- Document the process to invalidate/regenerate compromised credentials.
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (5)

108-176: Unify sheet positions; avoid shadowing the property.

Hard-coded positions appear in multiple places, and guard let contentViewTopConstraint = contentViewTopConstraint shadows the property. Centralize positions and use a non‑shadowing local.

Apply:

-    private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
-        guard let contentViewTopConstraint = contentViewTopConstraint else {
+    private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
+        guard let topConstraint = contentViewTopConstraint else {
             return
         }
@@
-        let currentY = contentViewTopConstraint.constant
+        let currentY = topConstraint.constant
@@
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75
-            let kDefaultBottomHalfPosition = screenHeight * 0.35
-            let kDefaultOpenedMapPosition = screenHeight * 0.2
+            let pos = sheetPositions()
+            let kDefaultClosedMapPosition = pos.closed
+            let kDefaultBottomHalfPosition = pos.half
+            let kDefaultOpenedMapPosition = pos.opened
@@
-            contentViewTopConstraint.constant = max(minY, min(maxY, newY))
+            topConstraint.constant = max(minY, min(maxY, newY))
@@
-            let finalCurrentY = contentViewTopConstraint.constant
-            let screenHeight = view.frame.size.height
-            let kDefaultClosedMapPosition = screenHeight * 0.75
-            let kDefaultBottomHalfPosition = screenHeight * 0.35
-            let kDefaultOpenedMapPosition = screenHeight * 0.2
+            let finalCurrentY = topConstraint.constant
+            let pos = sheetPositions()
+            let kDefaultClosedMapPosition = pos.closed
+            let kDefaultBottomHalfPosition = pos.half
+            let kDefaultOpenedMapPosition = pos.opened
@@
-                contentViewTopConstraint.constant = finalY
+                topConstraint.constant = finalY

Add once (outside this hunk):

private func sheetPositions() -> (opened: CGFloat, half: CGFloat, closed: CGFloat) {
    let h = view.bounds.height
    return (opened: h * 0.2, half: h * 0.35, closed: h * 0.75)
}

Also applies to: 219-226


178-191: Harden URL normalization (trim, case-insensitive scheme, encoding).

Current check is case-sensitive and doesn’t trim/encode.

Apply:

-        guard let website = pointOfUse.website else { return }
-
-        // Normalize URL by adding https scheme if missing
-        let normalizedWebsite: String
-        if website.hasPrefix("http://") || website.hasPrefix("https://") {
-            normalizedWebsite = website
-        } else {
-            normalizedWebsite = "https://" + website
-        }
-
-        guard let url = URL(string: normalizedWebsite) else { return }
+        guard var website = pointOfUse.website?.trimmingCharacters(in: .whitespacesAndNewlines),
+              !website.isEmpty else { return }
+        if !website.lowercased().hasPrefix("http://") && !website.lowercased().hasPrefix("https://") {
+            website = "https://" + website
+        }
+        guard var comps = URLComponents(string: website) else { return }
+        comps.percentEncodedPath = comps.percentEncodedPath // force encoding if needed
+        guard let url = comps.url else { return }

56-60: Clamp negative inset.

mapView.frame.height - detailsView.frame.height - 10 can be negative. Clamp to ≥ 0.

Apply:

-            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: mapView.frame.height - detailsView.frame.height - 10,
-                                               right: 0)
+            let bottom = max(0, mapView.frame.height - detailsView.frame.height - 10)
+            mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0)

299-302: Replace print with logger and gate for DEBUG.

Avoid prints in production.

Apply:

-            print("Warning: Failed to create detailsView for pointOfUse category: \(pointOfUse.category)")
+            #if DEBUG
+            DSLogger.log("Warning: Failed to create detailsView for pointOfUse category: \(pointOfUse.category)")
+            #endif

448-456: UI on main actor.

tryRefreshCtxToken() shows a modal; mark main actor to avoid UI off-main.

Apply:

-    private func tryRefreshCtxToken() async throws -> Bool {
+    @MainActor
+    private func tryRefreshCtxToken() async throws -> Bool {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cba31c3 and 7cce1e1.

📒 Files selected for processing (3)
  • .claude/agents/CLAUDE.md (1 hunks)
  • DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1 hunks)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (12 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-05T04:46:12.717Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#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/agents/CLAUDE.md
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is confirmed unused after code analysis. Radius functionality is properly handled through model.currentRadius and filters?.currentRadius, making the controller property redundant and safe to remove.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
📚 Learning: 2025-09-05T18:32:21.463Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift:67-67
Timestamp: 2025-09-05T18:32:21.463Z
Learning: The radius property in ExplorePointOfUseListViewController is unused and should be removed. Radius functionality is handled through the model.currentRadius and filters system instead of a controller-level property.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
📚 Learning: 2025-09-12T21:24:36.691Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#729
File: DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Model/AllMerchantLocationsDataProvider.swift:89-89
Timestamp: 2025-09-12T21:24:36.691Z
Learning: The pointOfUseId computed property on ExplorePointOfUse acts as an abstraction that returns the appropriate identifier based on the category - for merchants it returns merchantId, making direct calls with pointOfUseId equivalent to using merchantId for merchant entries.

Applied to files:

  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift
🧬 Code graph analysis (1)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift (4)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Views/ExploreMapView.swift (4)
  • mapView (250-255)
  • mapView (257-267)
  • mapView (283-289)
  • show (139-180)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swift (1)
  • show (142-148)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/PointOfUseDetailsView.swift (3)
  • callAction (138-159)
  • websiteAction (177-181)
  • setupGrabberPanGesture (1175-1183)
DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (1)
  • getMerchant (195-223)
🪛 markdownlint-cli2 (0.17.2)
.claude/agents/CLAUDE.md

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

(MD040, fenced-code-language)

🪛 SwiftLint (0.57.0)
DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/PointOfUseDetailsViewController.swift

[Warning] 38-38: Implicitly unwrapped optionals should be avoided when possible

(implicitly_unwrapped_optional)


[Warning] 40-40: Ensure declarations have a lower access control level than their enclosing parent

(lower_acl_than_parent)


[Warning] 59-59: Function parameters should be aligned vertically if they're in multiple lines in a method call

(vertical_parameter_alignment_on_call)


[Warning] 281-281: Force unwrapping should be avoided

(force_unwrapping)

🔇 Additional comments (6)
.claude/agents/CLAUDE.md (2)

210-211: Confirmed: kDefaultRadius = 32000 (meters)

Defined at DashWallet/Sources/Models/Explore Dash/ExploreDash.swift:24 as let kDefaultRadius: Double = 32000 — use this constant instead of hardcoded values.


159-163: Confirm Rust toolchain requirement.

Automated search returned "No files were searched", so presence of Cargo.toml or .rs sources could not be confirmed. If there is no Rust code, remove the "Rust toolchain (for DashSync integration)" bullet in .claude/agents/CLAUDE.md (lines 159–163). If Rust is required, document the exact crate/Cargo.toml paths and minimal setup steps (rustup/toolchain) that justify the requirement.

DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift (2)

97-99: Verify deviceUUID lifecycle before deleting on logout.

If the backend uses deviceUUID for device trust/rate‑limiting, wiping it on logout may cause unexpected behavior on next login.

Do we intend deviceUUID to persist across sessions? If yes, keep it; if not, ensure server expectations are aligned.


90-91: Call server logout before clearing local tokens (logout endpoint exists).
CTXSpendEndpoint already exposes case logout (DashWallet/Sources/Models/Explore Dash/Services/CTXSpendEndpoint.swift). Update CTXSpendService.logout() (DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift — ~lines 90–91) to perform a best‑effort request to the logout endpoint, handle/fall back on failures, then clear local tokens; remove or update the comment "// Clear local tokens (logout API does not exist)".

Likely an incorrect or invalid review comment.

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

93-106: Call action hardening looks good.

Digits-only sanitization, canOpenURL, and early‑exits are correct.


327-330: Constraint change: verify on small screens/Dynamic Type.

Removing the bottom constraint avoids compression, but can lead to ambiguous height on short content. Please test iPhone SE and large fonts.

bfoss765 and others added 2 commits September 16, 2025 10:19
- Restore CTX logout endpoint and server call functionality to original state
- No changes should have been made to CTX logout behavior - only UI text location changed
- Move CLAUDE.md to root folder per Anthropic best practices
- CTX logout now properly attempts server logout before local token cleanup

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

Co-Authored-By: Claude <[email protected]>
- Remove server logout attempt and async Task wrapper
- Implement synchronous logout that only clears local authentication data
- Remove unused logout endpoint from CTXSpendEndpoint.swift
- Forces complete re-authentication flow when user logs in again

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

Co-Authored-By: Claude <[email protected]>
Copy link

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

🧹 Nitpick comments (4)
CLAUDE.md (4)

172-176: Add language to fenced code block (fixes markdownlint MD040).

Specify a language for the directory listing code fence.

-```
+```text
 ../DashSync/          # Core Dash protocol library
 ../dapi-grpc/         # gRPC API definitions  
 ../dashwallet-ios/    # This repository

---

`26-32`: **Provide a concrete xcodebuild example for non-Fastlane runs.**

The code block contains only comments. Add a reproducible CLI for CI/local use.



```diff
 # Or in Xcode, run tests on iPhone 8 simulator (as configured in Fastfile)
+#
+# CLI alternative (without Fastlane):
+xcodebuild \
+  -workspace DashWallet.xcworkspace \
+  -scheme dashwallet \
+  -destination 'platform=iOS Simulator,name=iPhone 8' \
+  test

186-191: Strengthen “Security” with secrets-handling guidance.

Given recent concerns about exposed Uphold secrets, add explicit do/don’t guidance (env/xcconfig, Keychain, no secrets in repo/logs).

 ### Security
 - Jailbreak detection
 - Hardware encryption usage
 - Private key protection
 - Secure enclave integration
+ - Secrets management:
+   - Store API keys in per-env .xcconfig (not in repo), or fetch at runtime from a secure backend.
+   - Never commit secrets to source, Info.plist, or sample configs.
+   - Use Keychain for persisted tokens; avoid logging tokens/user PII.
+   - Use GitHub Actions/CI secrets; prohibit echoing secrets in logs.

225-230: Augment the review checklist with security/privacy checks.

Add checks for secrets and PII/logging to make this actionable during PRs.

 - [ ] Search for `!` force unwraps in location/coordinate handling  
 - [ ] Verify template images use proper rendering mode
 - [ ] Check radius constants are consistent across codebase
 - [ ] Ensure compactMap closures have explicit types if needed
 - [ ] Remove unused properties that create inconsistency
+ - [ ] Scan diffs for API keys/secrets; ensure none are committed
+ - [ ] Verify logs don’t include addresses, usernames, tokens, or PII
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cce1e1 and b734184.

📒 Files selected for processing (1)
  • CLAUDE.md (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-05T04:46:12.717Z
Learnt from: HashEngineering
PR: dashpay/dashwallet-ios#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
🪛 markdownlint-cli2 (0.17.2)
CLAUDE.md

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

(MD040, fenced-code-language)

🔇 Additional comments (2)
CLAUDE.md (2)

159-163: Verify minimum toolchain targets (Xcode/iOS).

Confirm Xcode 16+ and iOS 14.0+ are still the project’s enforced minima across all targets/schemes before we institutionalize them here.


98-101: Validate referenced paths/names match the repo.

Confirm “SwiftUI Components/” and “UIHostingController+DashWallet.swift” exist at these exact paths/names to avoid drift in docs.

Copy link
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.

Looks fine, we will merge this in a few days.

@bfoss765
Copy link
Contributor Author

bfoss765 commented Oct 2, 2025

These changes were implemented in PR732

@bfoss765 bfoss765 closed this Oct 2, 2025
@bfoss765 bfoss765 deleted the improvement/merchant-country-info branch October 2, 2025 20:46
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.

3 participants