Skip to content

feat: Maya entry points, Buy & Sell portal redesign, v8.6.0#753

Merged
bfoss765 merged 3 commits into
feat/mayafrom
feat/maya-entry-points
Feb 27, 2026
Merged

feat: Maya entry points, Buy & Sell portal redesign, v8.6.0#753
bfoss765 merged 3 commits into
feat/mayafrom
feat/maya-entry-points

Conversation

@bfoss765
Copy link
Copy Markdown
Contributor

@bfoss765 bfoss765 commented Feb 26, 2026

Summary

  • Maya integration: Add Maya as a new service in the Buy & Sell portal with a dedicated Maya portal screen for crypto conversion
  • Buy & Sell portal redesign: Replace storyboard-based UICollectionView with SwiftUI view matching Figma designs — grouped service cards (Uphold+Coinbase, Topper with "Powered by Uphold" badge, Maya), full-screen presentation, proper back chevron navigation
  • Shortcut bar customization: Add banner prompting users to customize shortcuts, with selection sheet for choosing shortcut actions
  • New SVG icon assets: ATM, Coinbase, CrowdNode, Topper, Uphold, Maya, and more shortcut icons
  • Version bump: 8.5.5 → 8.6.0

Test plan

  • Tap "Buy & Sell" shortcut on Home → verify full-screen presentation with back chevron, grouped cards matching Figma design
  • Tap each service (Uphold, Coinbase, Topper, Maya) → verify correct navigation
  • Tap Maya → verify Maya portal screen shows with back chevron and proper spacing
  • Navigate to Buy & Sell from Main Menu → verify back chevron pops correctly
  • Verify shortcut bar customization banner appears and selection sheet works
  • Verify all new shortcut icons render correctly
  • Verify version shows 8.6.0 in Settings/About

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Buy/Sell Portal added with Maya (crypto swaps), Uphold, Coinbase and Topper entry points.
    • Maya portal and related Convert Dash entry added.
    • Shortcut customization banner and long-press shortcut customization flow; new shortcuts: Send, ATM, Send to Contact, CrowdNode, Coinbase, Uphold, Topper.
  • UI/UX Improvements

    • Improved shortcut responsive layout and updated shortcut assets/icons with preserved SVG rendering.
  • Documentation

    • Updated Figma MCP setup and asset workflow guidance for a UI-driven setup and clearer troubleshooting.

…sion to 8.6.0

- Replace storyboard-based Buy & Sell screen with SwiftUI view matching Figma designs
- Add Maya as a new service in Buy & Sell portal with dedicated Maya portal screen
- Group services into cards: Uphold+Coinbase, Topper with "Powered by Uphold" badge, Maya
- Fix navigation: back chevron works for both modal dismiss and push pop
- Present Buy & Sell portal full screen instead of sheet overlay
- Add shortcut bar customization with banner and selection sheet
- Add new shortcut icon SVG assets (ATM, Coinbase, CrowdNode, Topper, Uphold, etc.)
- Bump version to 8.6.0

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

coderabbitai Bot commented Feb 26, 2026

Important

Review skipped

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

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

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Maya portal and Buy/Sell SwiftUI portal, shortcut customization banner and long-press shortcut flows, new shortcut actions and assets, Figma MCP switched from exec config to URL, project file and version bump, and global option state for banner lifecycle.

Changes

Cohort / File(s) Summary
MCP Configuration & Documentation
\.mcp.json, CLAUDE.md
Replaces executable-based MCP config with a URL field (http://127.0.0.1:3845/mcp) and updates CLAUDE.md to document UI-driven Figma Desktop MCP setup, renamed MCP tool checks, expanded troubleshooting, and asset/Contents.json guidance.
Project & Version Metadata
DashWallet.xcodeproj/project.pbxproj, DashSyncCurrentCommit
Adds PBX references/groups for new Swift files, updates MARKETING_VERSION from 8.5.58.6.0, and updates DashSyncCurrentCommit hash.
Global Options
DashWallet/Sources/Models/DWGlobalOptions.h, .../DWGlobalOptions.m, DashWallet/AppDelegate.m
Adds shortcutBannerState persisted option with init/restore logic and initializes/advances state in AppDelegate on launch.
Buy/Sell Portal & Service Model
DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift, .../BuySellPortalViewController.swift, .../Model/BuySellPortalModel.swift, .../Model/ServiceDataProvider.swift
Introduces SwiftUI BuySellPortalView, refactors controller to host SwiftUI, and adds maya service enum with UI and data-provider inclusion.
Maya Portal UI & Controller
DashWallet/Sources/UI/Maya/MayaPortalView.swift, .../MayaPortalViewController.swift
Adds MayaPortalView SwiftUI and MayaPortalViewController that hosts it via UIHostingController.
Home / Shortcuts UI & Behavior
DashWallet/Sources/UI/Home/*, DashWallet/Sources/UI/Home/Views/Shortcuts/*
Adds banner presentation (ShortcutCustomizeBannerView), selection UI (ShortcutSelectionView), banner state + lifecycle in HomeViewModel and HomeHeaderView, long-press handling in ShortcutsView, new long-press delegate method and shortcut selection flows, and replaces payToAddress with send in generation paths.
Shortcut Actions Model
DashWallet/Sources/UI/Home/Views/Shortcuts/Models/ShortcutAction.swift
Adds new ShortcutActionType cases: send, atm, sendToContact, crowdNode, coinbase, uphold, topper, and a customizableActions list.
Buy/Sell Controller Changes (navigation/behavior)
DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift
Controller now conforms to navigation protocols, exposes shouldPopViewController / isBackButtonHidden, embeds SwiftUI hosting controller, and wires Maya action to push Maya portal controller.
Service Overview Updates
DashWallet/Sources/UI/Coinbase/ServiceOverview/ServiceEntryPointModel.swift
Adds maya cases across computed properties (icons, titles, supportedFeatures placeholders) to include Maya in service entry points.
Merchant List Override
DashWallet/Sources/UI/Explore Dash/.../MerchantListViewController.swift
Adds optional initialSegment to allow caller-defined default segment, preferring it over location-based default.
Assets / Catalog Manifests
DashWallet/Resources/AppAssets.xcassets/Shortcuts/*, DashWallet/Resources/AppAssets.xcassets/{convert.crypto,maya.logo,portal.maya}.imageset/Contents.json
Adds/updates multiple Contents.json asset manifests for SVG icons, setting preserves-vector-representation: true and template-rendering-intent: "original" to preserve colors for UIKit/SwiftUI rendering.
UI Cell Fonts
DashWallet/Sources/UI/Home/Views/Shortcuts/DWShortcutCollectionViewCell~*.xib
Updates label font to system medium 12pt and disables adjustsFontForContentSizeCategory in iPad/iPhone XIBs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomeVC as HomeViewController
    participant VM as HomeViewModel
    participant Global as DWGlobalOptions
    participant Banner as ShortcutCustomizeBannerView
    participant Modal as ShortcutSelectionView

    User->>HomeVC: Open Home
    HomeVC->>VM: checkShortcutBanner()
    VM->>Global: read shortcutBannerState
    Global-->>VM: state
    VM->>HomeVC: shouldShowShortcutBanner = true
    HomeVC->>Banner: display banner
    User->>Banner: tap banner
    Banner->>HomeVC: request selection
    HomeVC->>Modal: present ShortcutSelectionView
    User->>Modal: select action
    Modal->>HomeVC: onSelect(action)
    HomeVC->>VM: update shortcuts
    VM->>Global: update shortcutBannerState
    VM->>HomeVC: shouldShowShortcutBanner = false
    HomeVC->>Banner: dismiss banner
Loading
sequenceDiagram
    participant User
    participant BuySellVC as BuySellPortalViewController
    participant Host as UIHostingController
    participant Portal as BuySellPortalView
    participant MayaVC as MayaPortalViewController

    User->>BuySellVC: Navigate to Buy/Sell
    BuySellVC->>Host: create hosting controller with BuySellPortalView
    Host->>Portal: render services (Uphold, Coinbase, Topper, Maya)
    User->>Portal: tap Maya
    Portal->>BuySellVC: mayaAction closure
    BuySellVC->>MayaVC: push MayaPortalViewController
    User->>MayaVC: interact with Maya Portal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped to code where portals bloom,

Maya peeks from room to room.
Banners bounce and shortcuts play,
SwiftUI leads the merry way.
Assets gleam and states align—hop, hooray! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: Maya integration, Buy & Sell portal redesign, and version bump to 8.6.0.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/maya-entry-points

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

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/Buy Sell/BuySellPortalViewController.swift (2)

73-96: ⚠️ Potential issue | 🟡 Minor

Avoid force unwrapping navigationController.

Line 79 uses self.navigationController!.popToViewController(...) which will crash if navigationController is nil. Use optional chaining instead.

🛡️ Proposed fix
             vc.userSignedOutBlock = { [weak self] isNeedToShowSignOutError in
                 guard let self else { return }

-                self.navigationController!.popToViewController(self, animated: true)
+                self.navigationController?.popToViewController(self, animated: true)

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

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift around
lines 73 - 96, The navigateToCoinbase method force-unwraps navigationController
when calling popToViewController inside the userSignedOutBlock; change this to
safely handle a nil navigationController (e.g., use optional chaining or guard
let navigationController = self.navigationController before calling
popToViewController) so the app won't crash if navigationController is nil;
update the userSignedOutBlock closure in navigateToCoinbase to use the safe
reference for navigationController (or return early) and keep the rest of the
logic (hiding tab bar, pushing view controllers, showing alert) unchanged.

98-105: ⚠️ Potential issue | 🟡 Minor

Avoid force unwrapping bundle info.

Line 100 uses double force unwrap (infoDictionary! and as!) which could crash if CFBundleDisplayName is missing. The same pattern is safely implemented in HomeViewController+Shortcuts.showTopper() using guard.

🛡️ Proposed fix
     `@objc`
     func topperAction() {
-        let urlString = topperViewModel.topperBuyUrl(walletName: Bundle.main.infoDictionary!["CFBundleDisplayName"] as! String)
-        if let url = URL(string: urlString) {
+        guard let bundleName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String,
+              let url = URL(string: topperViewModel.topperBuyUrl(walletName: bundleName)) else { return }
-            let safariViewController = SFSafariViewController.dw_controller(with: url)
-            present(safariViewController, animated: true)
-        }
+        let safariViewController = SFSafariViewController.dw_controller(with: url)
+        present(safariViewController, animated: true)
     }

As per coding guidelines: "Use guard statements instead of force unwrapping for optional properties."

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

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift around
lines 98 - 105, In topperAction(), avoid force-unwrapping
Bundle.main.infoDictionary and the CFBundleDisplayName cast; use a guard to
safely extract the display name (e.g., let name =
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String) and
return early if missing, then call topperViewModel.topperBuyUrl(walletName:
name) and proceed to create and present the SFSafariViewController; update
references to topperAction and topperViewModel.topperBuyUrl to use the guarded
value.
🧹 Nitpick comments (9)
DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_atm.imageset/Contents.json (1)

12-15: Verify rendering intent for this non-brand shortcut icon.

On Line 14, consider "template" instead of "original" if shortcut_atm should follow system/app tint like other generic shortcut actions.

Possible change if tinting is intended
   "properties" : {
     "preserves-vector-representation" : true,
-    "template-rendering-intent" : "original"
+    "template-rendering-intent" : "template"
   }

Based on learnings: In iOS XCAssets, use template for generic/system UI icons and original for brand-identity icons to preserve intended tint behavior.

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

In
`@DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_atm.imageset/Contents.json`
around lines 12 - 15, The asset's "template-rendering-intent" is set to
"original" for the shortcut_atm imageset; if this shortcut is a
generic/system-style icon that should receive app/system tinting, change the
value of "template-rendering-intent" from "original" to "template" in the
shortcut_atm imageset's Contents.json so iOS treats it as a template-rendered
image; leave as "original" only if this icon must preserve its brand colors.
DashWallet/Sources/Models/DWGlobalOptions.h (1)

62-65: Consider converting raw integer state values to a typed enum for type safety and clarity.

The shortcutBannerState property uses raw NSInteger with numeric literals (0–3) spread across multiple call sites in AppDelegate.m, HomeViewModel.swift, and initialization code. Define an NS_ENUM to replace magic numbers and prevent invalid state assignments.

♻️ Proposed refactor
+typedef NS_ENUM(NSInteger, DWShortcutBannerState) {
+    DWShortcutBannerStateNotInitialized = 0,
+    DWShortcutBannerStateNewInstallDeferred = 1,
+    DWShortcutBannerStateReadyToShow = 2,
+    DWShortcutBannerStateDismissed = 3,
+};
+
 /// Shortcut customization banner state:
 /// 0 = not initialized, 1 = new install deferred, 2 = ready to show, 3 = dismissed
-@property (nonatomic, assign) NSInteger shortcutBannerState;
+@property (nonatomic, assign) DWShortcutBannerState shortcutBannerState;

Update all call sites in AppDelegate.m (lines 103, 106, 109, 111, 113), HomeViewModel.swift (lines 695, 704), and DWGlobalOptions.m (lines 72, 161) to use the named enum constants.

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

In `@DashWallet/Sources/Models/DWGlobalOptions.h` around lines 62 - 65, Replace
the raw NSInteger magic numbers used for shortcutBannerState with a typed
NS_ENUM to improve type safety and clarity: add an enum (e.g., typedef
NS_ENUM(NSInteger, DWShortcutBannerState) { DWShortcutBannerStateUninitialized =
0, DWShortcutBannerStateNewInstallDeferred = 1, DWShortcutBannerStateReadyToShow
= 2, DWShortcutBannerStateDismissed = 3 }); change the property declaration in
DWGlobalOptions.h from NSInteger shortcutBannerState to DWShortcutBannerState
shortcutBannerState, update DWGlobalOptions.m initialization and any assignments
to use the new enum constants, and update all call sites in AppDelegate (uses of
shortcutBannerState) and HomeViewModel.swift to use the named enum values
instead of raw 0–3 values; ensure any switch/defaults or persisted storage
conversions handle the enum safely to prevent invalid values.
DashWallet/AppDelegate.m (1)

101-114: Use named banner-state constants instead of raw 0/1/2.

The transition logic is clear, but magic numbers make this state machine harder to maintain and safer to regress later.

♻️ Refactor sketch
+static NSInteger const DWShortcutBannerStateUninitialized = 0;
+static NSInteger const DWShortcutBannerStateDeferredUntilSecondLaunch = 1;
+static NSInteger const DWShortcutBannerStateReadyToShow = 2;
+
-    if (bannerOptions.shortcutBannerState == 0) {
+    if (bannerOptions.shortcutBannerState == DWShortcutBannerStateUninitialized) {
         if (bannerOptions.shouldDisplayOnboarding) {
-            bannerOptions.shortcutBannerState = 1;
+            bannerOptions.shortcutBannerState = DWShortcutBannerStateDeferredUntilSecondLaunch;
         } else {
-            bannerOptions.shortcutBannerState = 2;
+            bannerOptions.shortcutBannerState = DWShortcutBannerStateReadyToShow;
         }
-    } else if (bannerOptions.shortcutBannerState == 1) {
-        bannerOptions.shortcutBannerState = 2;
+    } else if (bannerOptions.shortcutBannerState == DWShortcutBannerStateDeferredUntilSecondLaunch) {
+        bannerOptions.shortcutBannerState = DWShortcutBannerStateReadyToShow;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/AppDelegate.m` around lines 101 - 114, Replace the magic numeric
states used on DWGlobalOptions.shortcutBannerState with named constants (e.g.,
ShortcutBannerStateHidden, ShortcutBannerStateDeferred,
ShortcutBannerStateReady) and update all comparisons/assignments in the state
machine accordingly; add the constants as an enum or static consts in
DWGlobalOptions (or its header) and then change the checks in the AppDelegate
block (where shortcutBannerState is read/assigned) to use those symbolic names
instead of 0/1/2 so the intent is explicit and future regressions are less
likely.
DashWallet/Sources/UI/Maya/MayaPortalView.swift (2)

29-36: Consider extracting hardcoded color to a named constant.

The Maya brand color Color(red: 0.08, green: 0.11, blue: 0.25) is hardcoded. For consistency and maintainability, consider adding this as a named color in the asset catalog or as a Color extension.

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

In `@DashWallet/Sources/UI/Maya/MayaPortalView.swift` around lines 29 - 36,
Replace the hardcoded color literal used in the RoundedRectangle inside
MayaPortalView (the Color(red: 0.08, green: 0.11, blue: 0.25) in the ZStack)
with a named color; add a "MayaBrand" color to the asset catalog and use
Color("MayaBrand") or add a Color extension (e.g., Color.mayaBrand) and use that
instead so the UIColor is centralized for reuse and easier maintenance.

60-85: Fix trailing closure syntax to address SwiftLint warning.

SwiftLint flags the Button(action:) { } pattern when using trailing closure with labeled action: parameter. Use explicit closure syntax instead.

♻️ Proposed fix
-                    Button(action: {
-                        // Placeholder — Convert Dash action not yet implemented
-                    }) {
+                    Button {
+                        // Placeholder — Convert Dash action not yet implemented
+                    } label: {
                         HStack(spacing: 16) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Maya/MayaPortalView.swift` around lines 60 - 85,
SwiftLint warns about using a labeled `action:` trailing closure for `Button`;
update the `Button(action: { ... }) { ... }` usage to the explicit
trailing-closure form `Button { /* action */ } label: { /* label view */ }` so
the action closure is the first unnamed trailing closure and the label is
specified with `label:`; locate the `Button` declaration that wraps the
`Image("convert.crypto")` / `VStack` and change its declaration accordingly
without altering the inner views or paddings.
DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift (3)

204-209: Avoid force unwrapping URL.

Line 205 uses force unwrap on URL(string: UIApplication.openSettingsURLString)!. While this system constant is unlikely to fail, using safe unwrapping is preferred per coding guidelines.

🔧 Proposed fix
                 if DWLocationManager.shared.isPermissionDenied {
-                    let settingsUrl = URL(string: UIApplication.openSettingsURLString)!
-
-                    if UIApplication.shared.canOpenURL(settingsUrl) {
-                        UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil)
+                    if let settingsUrl = URL(string: UIApplication.openSettingsURLString),
+                       UIApplication.shared.canOpenURL(settingsUrl) {
+                        UIApplication.shared.open(settingsUrl)
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift around
lines 204 - 209, Replace the force-unwrapped URL creation in the
permission-denied branch with a safe unwrap: when checking
DWLocationManager.shared.isPermissionDenied in BuySellPortalViewController, use
guard/if let to create settingsUrl from UIApplication.openSettingsURLString and
only call UIApplication.shared.canOpenURL/open when the URL is non-nil; handle
the nil case gracefully (e.g., return, no-op, or log) so the app never
force-unwraps the URL.

253-263: Consider adding a timeout to prevent indefinite waiting.

nextEmittedPlacemark() will wait indefinitely if currentPlacemark never emits a non-nil value (e.g., if reverse geocoding consistently fails). Consider adding a timeout or fallback behavior.

💡 Example with timeout
private func nextEmittedPlacemark() async -> CLPlacemark? {
    return await withCheckedContinuation { continuation in
        var cancellable: AnyCancellable?
        let timeout = DispatchWorkItem {
            cancellable?.cancel()
            continuation.resume(returning: nil)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: timeout)
        
        cancellable = DWLocationManager.shared.$currentPlacemark
            .compactMap { $0 }
            .sink { placemark in
                timeout.cancel()
                continuation.resume(returning: placemark)
                cancellable?.cancel()
            }
    }
}

Then update isGeoblocked() to handle the nil case conservatively.

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

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift around
lines 253 - 263, The nextEmittedPlacemark() continuation can hang forever;
modify the function to use a timeout fallback (e.g., schedule a DispatchWorkItem
or Task.sleep) that cancels the Combine subscription and resumes the
continuation with nil after a short interval, change its signature to return an
optional CLPlacemark? and ensure the sink path cancels the timeout before
resuming, and then update isGeoblocked() to treat a nil placemark conservatively
(e.g., assume geoblocked or use a safe default) so the caller handles the
timeout case.

149-152: Prefer static over class in final class.

Since BuySellPortalViewController is marked final, use static func instead of class func for the factory method.

🔧 Proposed fix
     `@objc`
-    class func controller() -> BuySellPortalViewController {
+    static func controller() -> BuySellPortalViewController {
         BuySellPortalViewController()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift around
lines 149 - 152, The factory method declaration should use static rather than
class because BuySellPortalViewController is final: change the declaration from
"class func controller() -> BuySellPortalViewController" to "static func
controller() -> BuySellPortalViewController" and remove the `@objc` attribute (or,
if Objective‑C exposure is required, replace the approach with an Objective‑C
compatible wrapper) so the method signature compiles and follows the final-class
convention.
DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift (1)

18-20: Sort imports alphabetically.

Per SwiftLint's sorted_imports rule, imports should be sorted alphabetically.

🔧 Proposed fix
 import UIKit
-import SafariServices
-import SwiftUI
+import SafariServices
+import SwiftUI

Actually, the alphabetical order should be:

-import UIKit
-import SafariServices
-import SwiftUI
+import SafariServices
+import SwiftUI
+import UIKit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Home/HomeViewController`+Shortcuts.swift around lines
18 - 20, Sort the import declarations in HomeViewController+Shortcuts.swift to
satisfy SwiftLint's sorted_imports rule by reordering the three imports
alphabetically: place SafariServices first, then SwiftUI, then UIKit; update the
import block at the top of the file so the declarations are in that order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DashWallet.xcodeproj/project.pbxproj`:
- Around line 1646-1649: The PBX entries for MayaPortalView.swift and
MayaPortalViewController.swift use invalid object IDs containing the text "MAYA"
instead of 24-char uppercase hex; open the Xcode project, remove and re-add the
Maya files (MayaPortalView.swift, MayaPortalViewController.swift) in the project
navigator so Xcode regenerates proper PBX object IDs for the build file entries
(the PBXBuildFile entries referencing MayaPortalView.swift and
MayaPortalViewController.swift) and commit the updated project.pbxproj.
- Around line 1648-1649: MayaPortalViewController is a non-thin UIViewController
with UI setup that violates the SwiftUI-first rule; replace it with a SwiftUI
view (e.g., MayaPortalView) and remove the UIViewController subclass and its
build file entries (symbols: MayaPortalViewController,
MayaPortalViewController.swift, and the PBXBuildFile entries shown) so no new
UIKit ViewController is added; move the backgroundColor, navigationItem/title,
and any layout or business-logic code into the SwiftUI view or a ViewModel, use
SwiftUI NavigationStack/toolbar/title modifiers instead of navigationItem, and
update all targets to reference the new SwiftUI source file(s) rather than
MayaPortalViewController.swift.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalView.swift:
- Line 88: In BuySellPortalView's background Color initializer the RGB
components use integer division (176/255, 182/255, 188/255) which evaluates to
0; change those to floating-point values (e.g. 176.0/255.0 or 176/255.0) so the
red/green/blue parameters are correct floats and the background color renders as
intended; update the Color(.sRGB, red: ..., green: ..., blue: ..., opacity: 0.1)
call accordingly.

In `@DashWallet/Sources/UI/Explore` Dash/Merchants &
ATMs/List/MerchantListViewController.swift:
- Around line 104-107: The property infoButton declared as an implicitly
unwrapped optional should be changed to a regular optional (replace
UIBarButtonItem! with UIBarButtonItem?) in MerchantListViewController; update
any code that currently force-unwraps infoButton (e.g., occurrences of
infoButton! or implicit use) to safely unwrap or use optional chaining, and
ensure initialization occurs before use (for example set the bar button in
viewDidLoad or assign to navigationItem) so there are no remaining IUO usages.

In `@DashWallet/Sources/UI/Home/HomeViewController`+Shortcuts.swift:
- Around line 194-213: The Coinbase shortcut skips the geoblock check and allows
access for blocked countries; add the same geoblock verification used in
BuySellPortalViewController.coinbaseAction() before presenting Coinbase: in
showCoinbase() (or at the start of showCoinbaseAuthenticated()) check the
geographic block (the same GB check/flag used in
BuySellPortalViewController.coinbaseAction) and, if geoblocked, present the
ServiceOverviewViewController flow or block access instead of navigating to
IntegrationViewController; update the logic around
DSAuthenticationManager.sharedInstance().authenticate and
Coinbase.shared.isAuthorized to only proceed to showCoinbaseAuthenticated when
the geoblock check passes.

In `@DashWallet/Sources/UI/Home/Views/Home` Header View/HomeHeaderView.swift:
- Around line 71-74: The onDismiss closure currently calls
viewModel.dismissShortcutBanner() and then directly calls self?.hideBanner(),
causing duplicate dismissal because the shouldShowShortcutBanner subscriber also
reacts to dismissShortcutBanner(); remove the direct call to hideBanner() from
the ShortcutCustomizeBannerView onDismiss closures (both occurrences around the
banner initialization and the later block) and let the shouldShowShortcutBanner
subscriber perform the UI hide, or alternatively make hideBanner() idempotent
and check a flag before animating to prevent duplicate animations and duplicate
delegate updates; update references in the onDismiss closures
(ShortcutCustomizeBannerView, viewModel.dismissShortcutBanner(), hideBanner(),
shouldShowShortcutBanner) accordingly.

In `@DashWallet/Sources/UI/Home/Views/Shortcuts/Models/ShortcutAction.swift`:
- Around line 48-53: The array constant customizableActions on
ShortcutActionType is missing the newly added case sendToContact, causing the
customization list and the “13 features” comment to be out of sync; update the
static let customizableActions: [ShortcutActionType] array to include
.sendToContact (and verify the order/count matches the comment) so the new
shortcut is available for customization.

In
`@DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutCustomizeBannerView.swift`:
- Around line 41-47: The dismiss Button(action: onDismiss) in
ShortcutCustomizeBannerView is too small and lacks accessibility metadata;
update the Button to provide an accessibilityLabel (e.g., "Close" or localized
string), an accessibilityHint if appropriate, and
accessibilityIdentifier/traits, and increase its tappable area by adding padding
or a larger contentShape/frame so the hit target meets accessibility touch size
(e.g., expand from 24x24 to at least 44x44 while keeping the visible icon size
unchanged); modify the Button surrounding the Image(systemName: "xmark") to
include these accessibility properties and the larger hit area.

---

Outside diff comments:
In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift:
- Around line 73-96: The navigateToCoinbase method force-unwraps
navigationController when calling popToViewController inside the
userSignedOutBlock; change this to safely handle a nil navigationController
(e.g., use optional chaining or guard let navigationController =
self.navigationController before calling popToViewController) so the app won't
crash if navigationController is nil; update the userSignedOutBlock closure in
navigateToCoinbase to use the safe reference for navigationController (or return
early) and keep the rest of the logic (hiding tab bar, pushing view controllers,
showing alert) unchanged.
- Around line 98-105: In topperAction(), avoid force-unwrapping
Bundle.main.infoDictionary and the CFBundleDisplayName cast; use a guard to
safely extract the display name (e.g., let name =
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String) and
return early if missing, then call topperViewModel.topperBuyUrl(walletName:
name) and proceed to create and present the SFSafariViewController; update
references to topperAction and topperViewModel.topperBuyUrl to use the guarded
value.

---

Nitpick comments:
In `@DashWallet/AppDelegate.m`:
- Around line 101-114: Replace the magic numeric states used on
DWGlobalOptions.shortcutBannerState with named constants (e.g.,
ShortcutBannerStateHidden, ShortcutBannerStateDeferred,
ShortcutBannerStateReady) and update all comparisons/assignments in the state
machine accordingly; add the constants as an enum or static consts in
DWGlobalOptions (or its header) and then change the checks in the AppDelegate
block (where shortcutBannerState is read/assigned) to use those symbolic names
instead of 0/1/2 so the intent is explicit and future regressions are less
likely.

In
`@DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_atm.imageset/Contents.json`:
- Around line 12-15: The asset's "template-rendering-intent" is set to
"original" for the shortcut_atm imageset; if this shortcut is a
generic/system-style icon that should receive app/system tinting, change the
value of "template-rendering-intent" from "original" to "template" in the
shortcut_atm imageset's Contents.json so iOS treats it as a template-rendered
image; leave as "original" only if this icon must preserve its brand colors.

In `@DashWallet/Sources/Models/DWGlobalOptions.h`:
- Around line 62-65: Replace the raw NSInteger magic numbers used for
shortcutBannerState with a typed NS_ENUM to improve type safety and clarity: add
an enum (e.g., typedef NS_ENUM(NSInteger, DWShortcutBannerState) {
DWShortcutBannerStateUninitialized = 0, DWShortcutBannerStateNewInstallDeferred
= 1, DWShortcutBannerStateReadyToShow = 2, DWShortcutBannerStateDismissed = 3
}); change the property declaration in DWGlobalOptions.h from NSInteger
shortcutBannerState to DWShortcutBannerState shortcutBannerState, update
DWGlobalOptions.m initialization and any assignments to use the new enum
constants, and update all call sites in AppDelegate (uses of
shortcutBannerState) and HomeViewModel.swift to use the named enum values
instead of raw 0–3 values; ensure any switch/defaults or persisted storage
conversions handle the enum safely to prevent invalid values.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift:
- Around line 204-209: Replace the force-unwrapped URL creation in the
permission-denied branch with a safe unwrap: when checking
DWLocationManager.shared.isPermissionDenied in BuySellPortalViewController, use
guard/if let to create settingsUrl from UIApplication.openSettingsURLString and
only call UIApplication.shared.canOpenURL/open when the URL is non-nil; handle
the nil case gracefully (e.g., return, no-op, or log) so the app never
force-unwraps the URL.
- Around line 253-263: The nextEmittedPlacemark() continuation can hang forever;
modify the function to use a timeout fallback (e.g., schedule a DispatchWorkItem
or Task.sleep) that cancels the Combine subscription and resumes the
continuation with nil after a short interval, change its signature to return an
optional CLPlacemark? and ensure the sink path cancels the timeout before
resuming, and then update isGeoblocked() to treat a nil placemark conservatively
(e.g., assume geoblocked or use a safe default) so the caller handles the
timeout case.
- Around line 149-152: The factory method declaration should use static rather
than class because BuySellPortalViewController is final: change the declaration
from "class func controller() -> BuySellPortalViewController" to "static func
controller() -> BuySellPortalViewController" and remove the `@objc` attribute (or,
if Objective‑C exposure is required, replace the approach with an Objective‑C
compatible wrapper) so the method signature compiles and follows the final-class
convention.

In `@DashWallet/Sources/UI/Home/HomeViewController`+Shortcuts.swift:
- Around line 18-20: Sort the import declarations in
HomeViewController+Shortcuts.swift to satisfy SwiftLint's sorted_imports rule by
reordering the three imports alphabetically: place SafariServices first, then
SwiftUI, then UIKit; update the import block at the top of the file so the
declarations are in that order.

In `@DashWallet/Sources/UI/Maya/MayaPortalView.swift`:
- Around line 29-36: Replace the hardcoded color literal used in the
RoundedRectangle inside MayaPortalView (the Color(red: 0.08, green: 0.11, blue:
0.25) in the ZStack) with a named color; add a "MayaBrand" color to the asset
catalog and use Color("MayaBrand") or add a Color extension (e.g.,
Color.mayaBrand) and use that instead so the UIColor is centralized for reuse
and easier maintenance.
- Around line 60-85: SwiftLint warns about using a labeled `action:` trailing
closure for `Button`; update the `Button(action: { ... }) { ... }` usage to the
explicit trailing-closure form `Button { /* action */ } label: { /* label view
*/ }` so the action closure is the first unnamed trailing closure and the label
is specified with `label:`; locate the `Button` declaration that wraps the
`Image("convert.crypto")` / `VStack` and change its declaration accordingly
without altering the inner views or paddings.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 562dddd and fcf0596.

⛔ Files ignored due to path filters (13)
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_atm.imageset/atm.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_coinbase.imageset/coinbase.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_crowdNode.imageset/crowdNode.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_customize_banner.imageset/shortcuts.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_payToAddress.imageset/payToAddress.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_send.imageset/send.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_sendToContact.imageset/sendToContact.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_spend.imageset/spend.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_topper.imageset/topper.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_uphold.imageset/uphold.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/convert.crypto.imageset/convert.crypto.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/maya.logo.imageset/maya.logo.svg is excluded by !**/*.svg
  • DashWallet/Resources/AppAssets.xcassets/portal.maya.imageset/portal.maya.svg is excluded by !**/*.svg
📒 Files selected for processing (37)
  • .mcp.json
  • CLAUDE.md
  • DashSyncCurrentCommit
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/AppDelegate.m
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_atm.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_coinbase.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_crowdNode.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_customize_banner.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_payToAddress.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_send.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_sendToContact.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_topper.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_uphold.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/convert.crypto.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/maya.logo.imageset/Contents.json
  • DashWallet/Resources/AppAssets.xcassets/portal.maya.imageset/Contents.json
  • DashWallet/Sources/Models/DWGlobalOptions.h
  • DashWallet/Sources/Models/DWGlobalOptions.m
  • DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift
  • DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift
  • DashWallet/Sources/UI/Buy Sell/Model/BuySellPortalModel.swift
  • DashWallet/Sources/UI/Buy Sell/Model/ServiceDataProvider.swift
  • DashWallet/Sources/UI/Coinbase/ServiceOverview/ServiceEntryPointModel.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift
  • DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift
  • DashWallet/Sources/UI/Home/HomeViewController.swift
  • DashWallet/Sources/UI/Home/Views/Home Header View/HomeHeaderView.swift
  • DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
  • DashWallet/Sources/UI/Home/Views/Shortcuts/DWShortcutCollectionViewCell~ipad.xib
  • DashWallet/Sources/UI/Home/Views/Shortcuts/DWShortcutCollectionViewCell~iphone.xib
  • DashWallet/Sources/UI/Home/Views/Shortcuts/Models/ShortcutAction.swift
  • DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutCustomizeBannerView.swift
  • DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutSelectionView.swift
  • DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swift
  • DashWallet/Sources/UI/Maya/MayaPortalView.swift
  • DashWallet/Sources/UI/Maya/MayaPortalViewController.swift

Comment thread DashWallet.xcodeproj/project.pbxproj Outdated
Comment thread DashWallet.xcodeproj/project.pbxproj Outdated
Comment thread DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift Outdated
Comment thread DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift
- Replace invalid MAYA PBX object IDs with valid 24-char hex
- Fix integer division in Color initializer (176/255 → 176.0/255.0)
- Change infoButton from IUO to regular optional
- Add geoblock check to Coinbase shortcut entry point
- Add accessibility label and 44x44 hit target to banner dismiss button
- Add comment explaining why MayaPortalViewController wrapper is needed
  (UIHostingController hides UIKit nav bar when pushed directly)

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift (2)

79-79: ⚠️ Potential issue | 🟡 Minor

Avoid force unwrapping navigationController.

Force unwrapping navigationController! could cause a crash if the view controller is presented modally without a navigation controller (even if unlikely in current flows).

Proposed fix using optional chaining
-                self.navigationController!.popToViewController(self, animated: true)
+                self.navigationController?.popToViewController(self, animated: true)

As per coding guidelines: "Use guard statements instead of force unwrapping for optional properties... to prevent crashes."

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

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift at line 79,
Replace the force-unwrapped navigationController call so the app won't crash
when the VC has no navigation controller: check navigationController safely
(e.g., guard let nav = navigationController else { dismiss(animated: true) or
return }) and then call nav.popToViewController(self, animated: true) (or use
optional chaining navigationController?.popToViewController(self, animated:
true) if you prefer a no-op fallback); update the call site around
popToViewController(self, animated: true) in BuySellPortalViewController to use
this safe pattern.

256-266: ⚠️ Potential issue | 🟡 Minor

Add timeout to prevent indefinite suspension, but fix the safety semantics of the timeout fallback.

The underlying risk is real: if reverse geocoding fails silently, currentPlacemark never emits a non-nil value (due to .compactMap { $0 }), leaving the withCheckedContinuation hanging indefinitely. Adding a timeout is the right approach for iOS 15+.

However, the proposed fix has a critical flaw in its safety logic: returning an empty CLPlacemark() on timeout will result in isoCountryCode being nil, which causes the geoblock check to treat the timeout as permission granted (not geoblocked). For a true "safe default," the timeout case should either:

  • Return a non-nil placemark with a known restricted country code, or
  • Return an optional CLPlacemark? and treat nil as geoblocked in the caller

Consider using the optional approach, which is cleaner:

-    private func nextEmittedPlacemark() async -> CLPlacemark {
-        return await withCheckedContinuation { continuation in
-            var cancellable: AnyCancellable?
-            cancellable = DWLocationManager.shared.$currentPlacemark
-                .compactMap { $0 }
-                .sink { placemark in
-                    continuation.resume(returning: placemark)
-                    cancellable?.cancel()
-                }
-        }
-    }
+    private func nextEmittedPlacemark() async -> CLPlacemark? {
+        return await withCheckedContinuation { continuation in
+            var cancellable: AnyCancellable?
+            cancellable = DWLocationManager.shared.$currentPlacemark
+                .timeout(.seconds(10), scheduler: DispatchQueue.main)
+                .first()
+                .sink(
+                    receiveCompletion: { _ in
+                        continuation.resume(returning: nil)
+                    },
+                    receiveValue: { placemark in
+                        continuation.resume(returning: placemark)
+                        cancellable?.cancel()
+                    }
+                )
+        }
+    }

Then update the caller to treat nil as geoblocked.

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

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift around
lines 256 - 266, Change nextEmittedPlacemark() to return an optional
CLPlacemark? and add a timeout so the continuation cannot suspend forever: in
nextEmittedPlacemark() (which currently subscribes to
DWLocationManager.shared.$currentPlacemark with .compactMap { $0 } and resumes
the continuation on first emission) use an async timeout (Task.sleep or
Task.withTimeout pattern for iOS15+) and, on timeout, resume returning nil
rather than creating an empty CLPlacemark; then update the caller(s) that use
nextEmittedPlacemark() to treat a nil result as geoblocked/deny (safe default)
instead of allowing nil isoCountryCode to be interpreted as allowed.
🧹 Nitpick comments (5)
DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift (2)

18-20: Sort imports alphabetically.

Static analysis indicates imports should be sorted. Reorder to: SafariServices, SwiftUI, UIKit.

🔧 Suggested fix
 import UIKit
-import SafariServices
-import SwiftUI
+import SafariServices
+import SwiftUI

Wait, that's not right. The correct alphabetical order:

-import UIKit
-import SafariServices
-import SwiftUI
+import SafariServices
+import SwiftUI
+import UIKit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Home/HomeViewController`+Shortcuts.swift around lines
18 - 20, Reorder the import statements at the top of
HomeViewController+Shortcuts.swift so they are alphabetized as: import
SafariServices, import SwiftUI, import UIKit; replace the current block (import
UIKit, import SafariServices, import SwiftUI) with the alphabetized imports to
satisfy the static analysis rule.

234-240: Consider user feedback on silent failures.

Both guard statements return silently if CFBundleDisplayName is missing or URL construction fails. While unlikely in practice, this could leave users confused if tapping the shortcut does nothing.

💡 Optional improvement
 private func showTopper() {
-    guard let bundleName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String else { return }
+    guard let bundleName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String else {
+        `#if` DEBUG
+        print("🎯 showTopper: CFBundleDisplayName not found")
+        `#endif`
+        return
+    }
     let urlString = TopperViewModel.shared.topperBuyUrl(walletName: bundleName)
-    guard let url = URL(string: urlString) else { return }
+    guard let url = URL(string: urlString) else {
+        `#if` DEBUG
+        print("🎯 showTopper: Invalid URL - \(urlString)")
+        `#endif`
+        return
+    }
     let safariViewController = SFSafariViewController.dw_controller(with: url)
     present(safariViewController, animated: true)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Home/HomeViewController`+Shortcuts.swift around lines
234 - 240, The showTopper method silently returns when CFBundleDisplayName is
missing or URL(string:) fails; change it to present a user-facing fallback
(e.g., UIAlertController with a short message like "Unable to open offer, please
try again") instead of returning silently, and also log the failure for
diagnostics (use your app logger or os_log) mentioning "CFBundleDisplayName
missing" or "Invalid topper URL" and include
TopperViewModel.shared.topperBuyUrl(...) input for context; update showTopper to
attempt the alert presentation when either guard fails and only proceed to
create and present SFSafariViewController.dw_controller(with:) when URL creation
succeeds.
DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift (1)

62-65: Extract repeated card container styling into a shared modifier.

The same padding/background/corner/shadow stack is duplicated three times. Centralizing it reduces style drift and makes future design updates safer.

♻️ Proposed refactor
-                .padding(6)
-                .background(Color.secondaryBackground)
-                .cornerRadius(12)
-                .shadow(color: .shadow, radius: 10, x: 0, y: 5)
+                .modifier(BuySellPortalCardStyle())
...
-                .padding(6)
-                .background(Color.secondaryBackground)
-                .cornerRadius(12)
-                .shadow(color: .shadow, radius: 10, x: 0, y: 5)
+                .modifier(BuySellPortalCardStyle())
...
-                .padding(6)
-                .background(Color.secondaryBackground)
-                .cornerRadius(12)
-                .shadow(color: .shadow, radius: 10, x: 0, y: 5)
+                .modifier(BuySellPortalCardStyle())
private struct BuySellPortalCardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding(6)
            .background(Color.secondaryBackground)
            .cornerRadius(12)
            .shadow(color: .shadow, radius: 10, x: 0, y: 5)
    }
}

Also applies to: 91-94, 105-108

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

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalView.swift around lines 62 - 65,
Create a shared ViewModifier (e.g., BuySellPortalCardStyle) that encapsulates
the repeated
.padding(6).background(Color.secondaryBackground).cornerRadius(12).shadow(color:
.shadow, radius: 10, x: 0, y: 5) stack and replace the duplicated modifier
chains in BuySellPortalView with a single application of that modifier (either
via .modifier(BuySellPortalCardStyle()) or a convenience .cardStyle() View
extension); update the three places where the chain is repeated (the occurrences
around the existing modifier chain and the similar blocks referenced at the
other two locations) to use the new modifier so styling is centralized and
consistent.
DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift (2)

152-155: Prefer static over class in a final class.

SwiftLint flags this: since BuySellPortalViewController is final, use static func instead of class func. Note: @objc static func is valid Swift syntax and maintains Objective-C interoperability.

Proposed fix
     `@objc`
-    class func controller() -> BuySellPortalViewController {
+    static func controller() -> BuySellPortalViewController {
         BuySellPortalViewController()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift around
lines 152 - 155, Replace the declaration of the factory method in the final
class BuySellPortalViewController: change the method signature from "@objc class
func controller() -> BuySellPortalViewController" to use "static" instead of
"class" so it becomes "@objc static func controller() ->
BuySellPortalViewController"; keep the method body unchanged to preserve
behavior and Objective‑C interoperability.

208-208: Force unwrap flagged by SwiftLint.

While UIApplication.openSettingsURLString is a well-known constant that should always produce a valid URL, SwiftLint flags this force unwrap.

Proposed fix using guard
-                    let settingsUrl = URL(string: UIApplication.openSettingsURLString)!
-
-                    if UIApplication.shared.canOpenURL(settingsUrl) {
+                    guard let settingsUrl = URL(string: UIApplication.openSettingsURLString),
+                          UIApplication.shared.canOpenURL(settingsUrl) else {
+                        return
+                    }
+                    UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil)
-                        UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil)
-                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift at line
208, Replace the force-unwrap of URL(string:
UIApplication.openSettingsURLString) with safe optional binding: use guard let
(or if let) to create settingsUrl from UIApplication.openSettingsURLString and
handle the failure path (e.g., log an error and return or no-op) before using
settingsUrl; update the code around the settingsUrl variable in
BuySellPortalViewController (where settingsUrl is created) to avoid force
unwrapping and satisfy SwiftLint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalView.swift:
- Line 88: The SwiftLint operator-spacing rule is violated in the .background
Color initializer; update the division operands in the Color(.sRGB, red:
176.0/255.0, green: 182.0/255.0, blue: 188.0/255.0, opacity: 0.1) expression to
include spaces around the '/' operator (e.g. use 176.0 / 255.0, 182.0 / 255.0,
188.0 / 255.0) so the .background(Color(...)) call conforms to
SwiftLint/SwiftFormat rules.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift:
- Line 100: The line in BuySellPortalViewController that calls
topperViewModel.topperBuyUrl currently force-unwraps Bundle.main.infoDictionary
and the display name; change this to safely unwrap the bundle info and
CFBundleDisplayName (e.g., use guard let or if let to get infoDictionary and
cast CFBundleDisplayName as String) and provide a safe fallback (such as
Bundle.main.bundleIdentifier or an empty/default name) before passing the string
into topperViewModel.topperBuyUrl to avoid runtime crashes.

---

Outside diff comments:
In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift:
- Line 79: Replace the force-unwrapped navigationController call so the app
won't crash when the VC has no navigation controller: check navigationController
safely (e.g., guard let nav = navigationController else { dismiss(animated:
true) or return }) and then call nav.popToViewController(self, animated: true)
(or use optional chaining navigationController?.popToViewController(self,
animated: true) if you prefer a no-op fallback); update the call site around
popToViewController(self, animated: true) in BuySellPortalViewController to use
this safe pattern.
- Around line 256-266: Change nextEmittedPlacemark() to return an optional
CLPlacemark? and add a timeout so the continuation cannot suspend forever: in
nextEmittedPlacemark() (which currently subscribes to
DWLocationManager.shared.$currentPlacemark with .compactMap { $0 } and resumes
the continuation on first emission) use an async timeout (Task.sleep or
Task.withTimeout pattern for iOS15+) and, on timeout, resume returning nil
rather than creating an empty CLPlacemark; then update the caller(s) that use
nextEmittedPlacemark() to treat a nil result as geoblocked/deny (safe default)
instead of allowing nil isoCountryCode to be interpreted as allowed.

---

Nitpick comments:
In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalView.swift:
- Around line 62-65: Create a shared ViewModifier (e.g., BuySellPortalCardStyle)
that encapsulates the repeated
.padding(6).background(Color.secondaryBackground).cornerRadius(12).shadow(color:
.shadow, radius: 10, x: 0, y: 5) stack and replace the duplicated modifier
chains in BuySellPortalView with a single application of that modifier (either
via .modifier(BuySellPortalCardStyle()) or a convenience .cardStyle() View
extension); update the three places where the chain is repeated (the occurrences
around the existing modifier chain and the similar blocks referenced at the
other two locations) to use the new modifier so styling is centralized and
consistent.

In `@DashWallet/Sources/UI/Buy` Sell/BuySellPortalViewController.swift:
- Around line 152-155: Replace the declaration of the factory method in the
final class BuySellPortalViewController: change the method signature from "@objc
class func controller() -> BuySellPortalViewController" to use "static" instead
of "class" so it becomes "@objc static func controller() ->
BuySellPortalViewController"; keep the method body unchanged to preserve
behavior and Objective‑C interoperability.
- Line 208: Replace the force-unwrap of URL(string:
UIApplication.openSettingsURLString) with safe optional binding: use guard let
(or if let) to create settingsUrl from UIApplication.openSettingsURLString and
handle the failure path (e.g., log an error and return or no-op) before using
settingsUrl; update the code around the settingsUrl variable in
BuySellPortalViewController (where settingsUrl is created) to avoid force
unwrapping and satisfy SwiftLint.

In `@DashWallet/Sources/UI/Home/HomeViewController`+Shortcuts.swift:
- Around line 18-20: Reorder the import statements at the top of
HomeViewController+Shortcuts.swift so they are alphabetized as: import
SafariServices, import SwiftUI, import UIKit; replace the current block (import
UIKit, import SafariServices, import SwiftUI) with the alphabetized imports to
satisfy the static analysis rule.
- Around line 234-240: The showTopper method silently returns when
CFBundleDisplayName is missing or URL(string:) fails; change it to present a
user-facing fallback (e.g., UIAlertController with a short message like "Unable
to open offer, please try again") instead of returning silently, and also log
the failure for diagnostics (use your app logger or os_log) mentioning
"CFBundleDisplayName missing" or "Invalid topper URL" and include
TopperViewModel.shared.topperBuyUrl(...) input for context; update showTopper to
attempt the alert presentation when either guard fails and only proceed to
create and present SFSafariViewController.dw_controller(with:) when URL creation
succeeds.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fcf0596 and db5290d.

📒 Files selected for processing (6)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift
  • DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift
  • DashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swift
  • DashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutCustomizeBannerView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • DashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swift

Comment thread DashWallet/Sources/UI/Buy Sell/BuySellPortalView.swift Outdated
Comment thread DashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swift Outdated
@bfoss765 bfoss765 changed the base branch from master to feat/maya February 27, 2026 17:53
Add operator spacing around division in BuySellPortalView and replace
force-unwraps with safe optional chaining in BuySellPortalViewController.

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

@HashEngineering HashEngineering left a comment

Choose a reason for hiding this comment

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

LGTM

@bfoss765 bfoss765 merged commit c70f97d into feat/maya Feb 27, 2026
2 checks passed
@bfoss765 bfoss765 deleted the feat/maya-entry-points branch February 27, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants