feat: Maya entry points, Buy & Sell portal redesign, v8.6.0#753
Conversation
…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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorAvoid force unwrapping
navigationController.Line 79 uses
self.navigationController!.popToViewController(...)which will crash ifnavigationControlleris 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 | 🟡 MinorAvoid force unwrapping bundle info.
Line 100 uses double force unwrap (
infoDictionary!andas!) which could crash ifCFBundleDisplayNameis missing. The same pattern is safely implemented inHomeViewController+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"ifshortcut_atmshould 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
templatefor generic/system UI icons andoriginalfor 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
shortcutBannerStateproperty uses rawNSIntegerwith numeric literals (0–3) spread across multiple call sites inAppDelegate.m,HomeViewModel.swift, and initialization code. Define anNS_ENUMto 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), andDWGlobalOptions.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 raw0/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 labeledaction: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 ifcurrentPlacemarknever 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: Preferstaticoverclassin final class.Since
BuySellPortalViewControlleris markedfinal, usestatic funcinstead ofclass funcfor 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_importsrule, imports should be sorted alphabetically.🔧 Proposed fix
import UIKit -import SafariServices -import SwiftUI +import SafariServices +import SwiftUIActually, 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
⛔ Files ignored due to path filters (13)
DashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_atm.imageset/atm.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_coinbase.imageset/coinbase.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_crowdNode.imageset/crowdNode.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_customize_banner.imageset/shortcuts.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_payToAddress.imageset/payToAddress.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_send.imageset/send.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_sendToContact.imageset/sendToContact.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_spend.imageset/spend.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_topper.imageset/topper.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_uphold.imageset/uphold.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/convert.crypto.imageset/convert.crypto.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/maya.logo.imageset/maya.logo.svgis excluded by!**/*.svgDashWallet/Resources/AppAssets.xcassets/portal.maya.imageset/portal.maya.svgis excluded by!**/*.svg
📒 Files selected for processing (37)
.mcp.jsonCLAUDE.mdDashSyncCurrentCommitDashWallet.xcodeproj/project.pbxprojDashWallet/AppDelegate.mDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_atm.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_coinbase.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_crowdNode.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_customize_banner.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_payToAddress.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_send.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_sendToContact.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_topper.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/Shortcuts/shortcut_uphold.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/convert.crypto.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/maya.logo.imageset/Contents.jsonDashWallet/Resources/AppAssets.xcassets/portal.maya.imageset/Contents.jsonDashWallet/Sources/Models/DWGlobalOptions.hDashWallet/Sources/Models/DWGlobalOptions.mDashWallet/Sources/UI/Buy Sell/BuySellPortalView.swiftDashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swiftDashWallet/Sources/UI/Buy Sell/Model/BuySellPortalModel.swiftDashWallet/Sources/UI/Buy Sell/Model/ServiceDataProvider.swiftDashWallet/Sources/UI/Coinbase/ServiceOverview/ServiceEntryPointModel.swiftDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swiftDashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swiftDashWallet/Sources/UI/Home/HomeViewController.swiftDashWallet/Sources/UI/Home/Views/Home Header View/HomeHeaderView.swiftDashWallet/Sources/UI/Home/Views/HomeViewModel.swiftDashWallet/Sources/UI/Home/Views/Shortcuts/DWShortcutCollectionViewCell~ipad.xibDashWallet/Sources/UI/Home/Views/Shortcuts/DWShortcutCollectionViewCell~iphone.xibDashWallet/Sources/UI/Home/Views/Shortcuts/Models/ShortcutAction.swiftDashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutCustomizeBannerView.swiftDashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutSelectionView.swiftDashWallet/Sources/UI/Home/Views/Shortcuts/ShortcutsView.swiftDashWallet/Sources/UI/Maya/MayaPortalView.swiftDashWallet/Sources/UI/Maya/MayaPortalViewController.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>
There was a problem hiding this comment.
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 | 🟡 MinorAvoid 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 | 🟡 MinorAdd timeout to prevent indefinite suspension, but fix the safety semantics of the timeout fallback.
The underlying risk is real: if reverse geocoding fails silently,
currentPlacemarknever emits a non-nil value (due to.compactMap { $0 }), leaving thewithCheckedContinuationhanging 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 inisoCountryCodebeingnil, 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 treatnilas geoblocked in the callerConsider 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
nilas 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 SwiftUIWait, 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
guardstatements return silently ifCFBundleDisplayNameis 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: Preferstaticoverclassin a final class.SwiftLint flags this: since
BuySellPortalViewControllerisfinal, usestatic funcinstead ofclass func. Note:@objc static funcis 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.openSettingsURLStringis 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
📒 Files selected for processing (6)
DashWallet.xcodeproj/project.pbxprojDashWallet/Sources/UI/Buy Sell/BuySellPortalView.swiftDashWallet/Sources/UI/Buy Sell/BuySellPortalViewController.swiftDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swiftDashWallet/Sources/UI/Home/HomeViewController+Shortcuts.swiftDashWallet/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
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>
Summary
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
UI/UX Improvements
Documentation