Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ TODO.md
*/Topper-Info.plist
*/Coinbase-Info.plist
*/ZenLedger-Info.plist
CLAUDE.md
6 changes: 6 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@
755049AA2C846299008FA7EB /* DWAboutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 755049A72C846299008FA7EB /* DWAboutViewController.m */; };
755049AC2C846576008FA7EB /* MenuItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755049AB2C846576008FA7EB /* MenuItemModel.swift */; };
755049AD2C846576008FA7EB /* MenuItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755049AB2C846576008FA7EB /* MenuItemModel.swift */; };
7556EE412DF9876B004E8093 /* ExploreSyncBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7556EE402DF9876B004E8093 /* ExploreSyncBannerView.swift */; };
7556EE422DF9876B004E8093 /* ExploreSyncBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7556EE402DF9876B004E8093 /* ExploreSyncBannerView.swift */; };
755A22BD2B1385FD001F170D /* IconAttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A22BC2B1385FD001F170D /* IconAttributedText.swift */; };
755B4B222B0C903500B844F0 /* DWDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755B4B212B0C903500B844F0 /* DWDateFormatter.swift */; };
755B4B232B0C903500B844F0 /* DWDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755B4B212B0C903500B844F0 /* DWDateFormatter.swift */; };
Expand Down Expand Up @@ -2491,6 +2493,7 @@
755049A72C846299008FA7EB /* DWAboutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWAboutViewController.m; sourceTree = "<group>"; };
755049A82C846299008FA7EB /* DWAboutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWAboutViewController.h; sourceTree = "<group>"; };
755049AB2C846576008FA7EB /* MenuItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemModel.swift; sourceTree = "<group>"; };
7556EE402DF9876B004E8093 /* ExploreSyncBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreSyncBannerView.swift; sourceTree = "<group>"; };
755A22BC2B1385FD001F170D /* IconAttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconAttributedText.swift; sourceTree = "<group>"; };
755B4B212B0C903500B844F0 /* DWDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DWDateFormatter.swift; sourceTree = "<group>"; };
755C32372C358FBD007DA721 /* BackupSeedPhraseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSeedPhraseViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5771,6 +5774,7 @@
47AE8BD328C1305E00490F5E /* MerchantListViewController.swift */,
47AE8BDD28C1305E00490F5E /* AtmListViewController.swift */,
47AE8BB728C1305E00490F5E /* AllMerchantLocationsViewController.swift */,
7556EE402DF9876B004E8093 /* ExploreSyncBannerView.swift */,
);
path = List;
sourceTree = "<group>";
Expand Down Expand Up @@ -8811,6 +8815,7 @@
C9F4520B2A1209D100825057 /* HomeHeaderModel.swift in Sources */,
47C6E6E02919578C003FEDF2 /* TerritoryListModel.swift in Sources */,
2AFF01DB243F4559003718DC /* DWDPRegistrationStatus.m in Sources */,
7556EE412DF9876B004E8093 /* ExploreSyncBannerView.swift in Sources */,
472D13ED299E6579006903F1 /* CurrencyExchanger_Objc.m in Sources */,
471DD1B8290A92CD00E030C8 /* Tools.swift in Sources */,
478A2C7128DC554200AD1420 /* BuySellPortalModel.swift in Sources */,
Expand Down Expand Up @@ -9468,6 +9473,7 @@
C9D2C76C2A320AA000D15901 /* AccountListController.swift in Sources */,
753261B02CBC11BF003CDE00 /* WelcomeViewController.swift in Sources */,
C9D2C76D2A320AA000D15901 /* DWUpholdTransactionObject.m in Sources */,
7556EE422DF9876B004E8093 /* ExploreSyncBannerView.swift in Sources */,
754C27CD2CC3C15B00BA7B9F /* FeatureSingleItem.swift in Sources */,
C943B5052A40A54600AF23C5 /* DWDPIncomingRequestCell.m in Sources */,
C9D2C76E2A320AA000D15901 /* DWBaseSeedViewController.m in Sources */,
Expand Down
18 changes: 17 additions & 1 deletion DashWallet/Sources/Models/Explore Dash/ExploreDash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,27 @@ public class ExploreDash {
removeCurrentDatabaseIfNeeded()

let isFileExists = FileManager.default.fileExists(atPath: destinationPath.path)
guard !isFileExists else { return }
guard !isFileExists else {
// Check if existing database has old schema and remove it
if ExploreDatabaseConnection.hasOldMerchantIdSchema(at: destinationPath) {
try? FileManager.default.removeItem(at: destinationPath)
// Don't copy bundled database as it also has old schema
// Wait for new database to be downloaded
return
}
return
}

guard let dbURL = Bundle.main.url(forResource: "explore", withExtension: "db") else {
throw ExploreDatabaseConnectionError.fileNotFound
}

// Check if bundled database has old schema before copying
if ExploreDatabaseConnection.hasOldMerchantIdSchema(at: dbURL) {
// Don't copy bundled database with old schema
// Wait for new database to be downloaded
return
}

try FileManager.default.copyItem(at: dbURL, to: destinationPath)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,42 @@ class ExploreDatabaseConnection {
try? self?.connect()
}
}

static func hasOldMerchantIdSchema(at url: URL) -> Bool {
do {
let db = try Connection(url.path)
// Query the sqlite_master table to get the schema
let query = "SELECT sql FROM sqlite_master WHERE type='table' AND name='merchant'"

if let row = try db.prepare(query).makeIterator().next(),
let sql = row[0] as? String {
// Check if merchantId is defined as INTEGER (old schema)
// New schema should have it as TEXT
return sql.contains("merchantId` INTEGER") || sql.contains("merchantId INTEGER")
}
} catch {
// If we can't check, assume it might be old to be safe
return true
}
return false
}

func connect() throws {
db = nil

guard let dbPath = dbPath() else { throw ExploreDatabaseConnectionError.fileNotFound }
guard let dbPath = dbPath() else {
// No database found - this is expected if we're waiting for v3 download
// Create an in-memory database to prevent crashes
db = try Connection(.inMemory)
return
}

do {
db = try Connection(nil ?? dbPath)
db = try Connection(dbPath)
} catch {
print(error)
// Fallback to in-memory database if connection fails
db = try Connection(.inMemory)
}
}

Expand All @@ -59,18 +85,34 @@ class ExploreDatabaseConnection {
}

func execute<Item: RowDecodable>(query: QueryType) throws -> [Item] {
let items = try db.prepare(query)
guard db != nil else { return [] }

do {
let items = try db.prepare(query)

var resultItems: [Item] = []
var resultItems: [Item] = []

for item in items {
resultItems.append(Item(row: item))
}
for item in items {
resultItems.append(Item(row: item))
}

return resultItems
return resultItems
} catch {
// If query fails (e.g., table doesn't exist in in-memory db), return empty array
print("Database query failed: \(error)")
return []
}
}

func execute<Item: RowDecodable>(query: String) throws -> [Item] {
try db.prepareRowIterator(query).map { Item(row: $0) }
guard db != nil else { return [] }

do {
return try db.prepareRowIterator(query).map { Item(row: $0) }
} catch {
// If query fails (e.g., table doesn't exist in in-memory db), return empty array
print("Database query failed: \(error)")
return []
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation
import SSZipArchive

// TODO: Move it to plist and note in release process
let gsFilePath = "gs://dash-wallet-firebase.appspot.com/explore/explore-v2.db"
let gsFilePath = "gs://dash-wallet-firebase.appspot.com/explore/explore-v3.db"

private let fileName = "explore"

Expand Down Expand Up @@ -136,10 +136,10 @@ extension ExploreDatabaseSyncManager {

private func unzipFile(at path: String, password: String) {
var error: NSError?
let urlToUnzip = getDocumentsDirectory()
let urlToUnzip = self.getDocumentsDirectory()
SSZipArchive.unzipFile(atPath: path, toDestination: urlToUnzip.path, preserveAttributes: true, overwrite: true,
nestedZipLevel: 0, password: password, error: &error, delegate: nil,
progressHandler: nil) { path, _, _ in
nestedZipLevel: 0, password: password, error: &error, delegate: nil,
progressHandler: nil) { path, _, _ in
NotificationCenter.default.post(name: ExploreDatabaseSyncManager.databaseHasBeenUpdatedNotification, object: nil)
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ + (NSString *)transactionURLFormat {

+ (NSString *)logoutURLString {
return @"https://uphold.com/";

}

@end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class ExplorePointOfUseListViewController: UIViewController {
internal var radius = 20 // In miles //Move to model
internal var mapView: ExploreMapView!
internal var showMapButton: UIButton!
internal var syncBannerView: ExploreSyncBannerView?
internal var syncBannerHeightConstraint: NSLayoutConstraint?

internal var contentViewTopLayoutConstraint: NSLayoutConstraint!
internal var contentView: UIView!
Expand Down Expand Up @@ -136,6 +138,9 @@ class ExplorePointOfUseListViewController: UIViewController {

showMapIfNeeded()
DWLocationManager.shared.add(observer: self)

// Check database sync status again in case it changed while navigating
checkDatabaseSyncStatus()
}

override func viewWillDisappear(_ animated: Bool) {
Expand All @@ -144,6 +149,7 @@ class ExplorePointOfUseListViewController: UIViewController {
super.viewWillDisappear(animated)

DWLocationManager.shared.remove(observer: self)
NotificationCenter.default.removeObserver(self, name: ExploreDatabaseSyncManager.databaseHasBeenUpdatedNotification, object: nil)
}

override func viewDidLoad() {
Expand Down Expand Up @@ -184,12 +190,76 @@ class ExplorePointOfUseListViewController: UIViewController {
}

configureHierarchy()

// Check database sync status
checkDatabaseSyncStatus()

// Observe database sync notifications
NotificationCenter.default.addObserver(self, selector: #selector(databaseHasBeenUpdated), name: ExploreDatabaseSyncManager.databaseHasBeenUpdatedNotification, object: nil)
}
}

extension ExplorePointOfUseListViewController {
@objc
internal func configureModel() { }

private func checkDatabaseSyncStatus() {
// Check if we need to show sync banner
// If database doesn't exist or has old schema, show banner
let documentsPath = FileManager.documentsDirectoryURL.appendingPathComponent(kExploreDashDatabaseName)
let fileExists = FileManager.default.fileExists(atPath: documentsPath.path)

// If file doesn't exist or has old schema, show sync banner
if !fileExists || ExploreDatabaseConnection.hasOldMerchantIdSchema(at: documentsPath) {
showSyncBanner()
} else {
hideSyncBanner()
}
}

private func showSyncBanner() {
guard syncBannerView?.isHidden == true else { return }

syncBannerView?.isHidden = false
syncBannerHeightConstraint?.constant = 30

UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}

private func hideSyncBanner() {
guard syncBannerView?.isHidden == false else { return }

syncBannerHeightConstraint?.constant = 0

UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
} completion: { _ in
self.syncBannerView?.isHidden = true
}
}

@objc private func databaseHasBeenUpdated() {
DispatchQueue.main.async { [weak self] in
// Database has been updated, hide the sync banner
self?.hideSyncBanner()

// The database connection needs to be re-established after update
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }

self.model.items = []
if let currentProvider = self.model.currentDataProvider {
currentProvider.items = []
currentProvider.currentPage = nil
}

self.model.refreshItems()
self.tableView.reloadData()
}
}
}
}

// MARK: DWLocationObserver
Expand Down Expand Up @@ -306,6 +376,12 @@ extension ExplorePointOfUseListViewController {
contentView.layer.cornerRadius = 20
contentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.addSubview(contentView)

// Add sync banner attached to app bar but overlaying content
syncBannerView = ExploreSyncBannerView()
syncBannerView?.translatesAutoresizingMaskIntoConstraints = false
syncBannerView?.isHidden = true
view.addSubview(syncBannerView!)

let stackView = UIStackView()
stackView.axis = .vertical
Expand Down Expand Up @@ -364,13 +440,23 @@ extension ExplorePointOfUseListViewController {

contentViewTopLayoutConstraint = contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: -handlerViewHeight)

syncBannerHeightConstraint = syncBannerView!.heightAnchor.constraint(equalToConstant: 0)
// Set high z-position to overlay content
syncBannerView!.layer.zPosition = 100

NSLayoutConstraint.activate([
contentViewTopLayoutConstraint,

contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),

// Sync banner constraints - attached to app bar, overlaying content
syncBannerView!.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
syncBannerView!.leadingAnchor.constraint(equalTo: view.leadingAnchor),
syncBannerView!.trailingAnchor.constraint(equalTo: view.trailingAnchor),
syncBannerHeightConstraint!,

handlerView.heightAnchor.constraint(equalToConstant: handlerViewHeight),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// Created by Andrei Ashikhmin
// Copyright © 2025 Dash Core Group. All rights reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import UIKit

class ExploreSyncBannerView: UIView {

private let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = NSLocalizedString("Sync in progress… Results may not be complete.", comment: "Explore Dash")
label.textColor = .white
label.font = .dw_font(forTextStyle: .footnote)
label.textAlignment = .center
label.numberOfLines = 1
return label
}()

override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}

private func setupView() {
backgroundColor = .dw_dashBlue()

addSubview(label)

NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
label.topAnchor.constraint(equalTo: topAnchor, constant: 8),
])
}
}
3 changes: 3 additions & 0 deletions DashWallet/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2374,6 +2374,9 @@
/* No comment provided by engineer. */
"Sync Failed" = "Sync Failed";

/* Explore Dash */
"Sync in progress… Results may not be complete." = "Sync in progress… Results may not be complete.";

/* Translate it as short as possible! (24 symbols max) */
"Sync Now" = "Sync Now";

Expand Down
Loading
Loading