Skip to content

Commit cd7e2b2

Browse files
authored
Merge pull request #711 from Syn-McJ/feat/dashspend-fixed-denoms
feat(dashspend): fixed denominations
2 parents 4b4f412 + dd51db4 commit cd7e2b2

55 files changed

Lines changed: 1172 additions & 230 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DashWallet.xcodeproj/project.pbxproj

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,8 @@
548548
753261AE2CBC1040003CDE00 /* InvitationFlowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753261AD2CBC1040003CDE00 /* InvitationFlowViewController.swift */; };
549549
753261B02CBC11BF003CDE00 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753261AF2CBC11BF003CDE00 /* WelcomeViewController.swift */; };
550550
753261B22CBC157F003CDE00 /* GetStartedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753261B12CBC157F003CDE00 /* GetStartedViewController.swift */; };
551+
75387B4A2E0400C300BCCC72 /* MerchantDenominations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75387B492E0400C300BCCC72 /* MerchantDenominations.swift */; };
552+
75387B4B2E0400C300BCCC72 /* MerchantDenominations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75387B492E0400C300BCCC72 /* MerchantDenominations.swift */; };
551553
753E46E82DE1E24300A3FF2A /* CTXSpendModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753E46E72DE1E24300A3FF2A /* CTXSpendModels.swift */; };
552554
753E46E92DE1E24300A3FF2A /* CTXSpendModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753E46E72DE1E24300A3FF2A /* CTXSpendModels.swift */; };
553555
753F75342DD0D42300D40DFE /* DashSpendPayScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753F75332DD0D41900D40DFE /* DashSpendPayScreen.swift */; };
@@ -2469,6 +2471,7 @@
24692471
753261AD2CBC1040003CDE00 /* InvitationFlowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitationFlowViewController.swift; sourceTree = "<group>"; };
24702472
753261AF2CBC11BF003CDE00 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
24712473
753261B12CBC157F003CDE00 /* GetStartedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStartedViewController.swift; sourceTree = "<group>"; };
2474+
75387B492E0400C300BCCC72 /* MerchantDenominations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MerchantDenominations.swift; sourceTree = "<group>"; };
24722475
753E46E72DE1E24300A3FF2A /* CTXSpendModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CTXSpendModels.swift; sourceTree = "<group>"; };
24732476
753F75332DD0D41900D40DFE /* DashSpendPayScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendPayScreen.swift; sourceTree = "<group>"; };
24742477
753F75362DD0D75F00D40DFE /* DashSpendPayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSpendPayViewModel.swift; sourceTree = "<group>"; };
@@ -6115,24 +6118,25 @@
61156118
isa = PBXGroup;
61166119
children = (
61176120
7581B19C2CE3509A00714007 /* Dialogs */,
6121+
75EBAA112BB99B6B004488E3 /* BottomSheet.swift */,
6122+
759ADD562BF3447400767ACD /* Button.swift */,
6123+
75CDD7852C08D61300F433D2 /* DashAmount.swift */,
61186124
750CEFA02CCA6EA100E87A32 /* TextInput.swift */,
61196125
75EBAA082BB9791B004488E3 /* Icon.swift */,
61206126
7566F4892BB6CAF2005238D2 /* MenuItem.swift */,
61216127
75EBAA0B2BB9792F004488E3 /* FeatureTopText.swift */,
61226128
754C27CB2CC3C14F00BA7B9F /* FeatureSingleItem.swift */,
61236129
75EBAA0E2BB99036004488E3 /* TextIntro.swift */,
6124-
75EBAA112BB99B6B004488E3 /* BottomSheet.swift */,
61256130
758C4C4E2DFAC1C30028549B /* RadioButtonRow.swift */,
61266131
75BDE7AB2BF3287400556791 /* Toast.swift */,
6127-
759ADD562BF3447400767ACD /* Button.swift */,
61286132
75AA33CE2BF9D44A00F12465 /* ButtonsGroup.swift */,
61296133
75AA33D12BF9E18E00F12465 /* Color+DWStyle.swift */,
61306134
75AA33D42BF9E1D400F12465 /* Font+DWStyle.swift */,
61316135
75AA33D72BFB4A5A00F12465 /* Extensions.swift */,
61326136
75CDD77F2C0898E400F433D2 /* Shape.swift */,
61336137
75CDD7822C0898FA00F433D2 /* Wrapper.swift */,
6134-
75CDD7852C08D61300F433D2 /* DashAmount.swift */,
61356138
759609222C455B2000F3BF04 /* SendIntro.swift */,
6139+
75387B492E0400C300BCCC72 /* MerchantDenominations.swift */,
61366140
7545ED5F2DA91FC60075F45C /* NumericKeyboardView.swift */,
61376141
756FE7092CDCD6CD00E6C195 /* ValidationCheck.swift */,
61386142
);
@@ -9049,6 +9053,7 @@
90499053
C956AF282A5CAD4D002FAB75 /* TitleCell.swift in Sources */,
90509054
C9F42FA429DBC6E5001BC549 /* PaymentsViewController.swift in Sources */,
90519055
4774DCDD28F43A68008CF87D /* ServiceDataSource.swift in Sources */,
9056+
75387B4A2E0400C300BCCC72 /* MerchantDenominations.swift in Sources */,
90529057
47838B7A2900196F0003E8AB /* ConverterView.swift in Sources */,
90539058
2A0C69CA23142E11001B8C90 /* DWModalBaseAnimation.m in Sources */,
90549059
C956AF262A5CACE6002FAB75 /* TitleValueCell.swift in Sources */,
@@ -9840,6 +9845,7 @@
98409845
C943B5082A40A54600AF23C5 /* DWDPTxListCell.m in Sources */,
98419846
75FFD6BC2BF48DF80032879E /* HomeViewController+JailbreakCheck.swift in Sources */,
98429847
C9D2C8942A320AA000D15901 /* DerivationPathKeysHeaderView.swift in Sources */,
9848+
75387B4B2E0400C300BCCC72 /* MerchantDenominations.swift in Sources */,
98439849
759063ED2C42687F002F2AA9 /* RootEditProfileViewController.swift in Sources */,
98449850
C9D2C8952A320AA000D15901 /* ConvertCryptoOrderPreviewModel.swift in Sources */,
98459851
75CDD7872C08D61300F433D2 /* DashAmount.swift in Sources */,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ class MerchantDAO: PointOfUseDAO {
5757
let typeColumn = ExplorePointOfUse.type
5858
let paymentMethodColumn = ExplorePointOfUse.paymentMethod
5959
let territoryColumn = ExplorePointOfUse.territory
60-
let denominationsTypeColumn = ExplorePointOfUse.denominationsType
6160

6261
var queryFilter = Expression<Bool>(value: true)
6362

@@ -73,6 +72,10 @@ class MerchantDAO: PointOfUseDAO {
7372
queryFilter = queryFilter && methods.map { $0.rawValue }.contains(paymentMethodColumn)
7473
}
7574

75+
// Filter out URL-based redemption merchants (not supported)
76+
// Using literal expression to handle cases where redeemType column might not exist
77+
queryFilter = queryFilter && Expression<Bool>(literal: "(redeemType IS NULL OR redeemType != 'url')")
78+
7679
// Add denomination type filter (only applies to gift card merchants)
7780
if let denominationType = denominationType {
7881
switch denominationType {

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ extension ExplorePointOfUse {
5959
let deeplink: String?
6060
let savingsBasisPoints: Int // in basis points 1 = 0.001%
6161
let denominationsType: String?
62+
let denominations: [Int]
63+
let redeemType: String?
64+
65+
init(merchantId: String, paymentMethod: PaymentMethod, type: `Type`, deeplink: String?, savingsBasisPoints: Int, denominationsType: String?, denominations: [Int] = [], redeemType: String?) {
66+
self.merchantId = merchantId
67+
self.paymentMethod = paymentMethod
68+
self.type = type
69+
self.deeplink = deeplink
70+
self.savingsBasisPoints = savingsBasisPoints
71+
self.denominationsType = denominationsType
72+
self.denominations = denominations
73+
self.redeemType = redeemType
74+
}
6275

6376
func toSavingPercentages() -> Double {
6477
return Double(savingsBasisPoints) / 100
@@ -193,6 +206,7 @@ extension ExplorePointOfUse: RowDecodable {
193206
static let manufacturer = Expression<String?>("manufacturer")
194207
static let savingPercentage = Expression<Int>("savingsPercentage")
195208
static let denominationsType = Expression<String?>("denominationsType")
209+
static let redeemType = Expression<String?>("redeemType")
196210

197211
init(row: Row) {
198212
let name = row[ExplorePointOfUse.name]
@@ -230,8 +244,9 @@ extension ExplorePointOfUse: RowDecodable {
230244
let deeplink = row[ExplorePointOfUse.deeplink]
231245
let savingsPercentage = row[ExplorePointOfUse.savingPercentage]
232246
let denominationsType = row[ExplorePointOfUse.denominationsType]
247+
let redeemType = row[ExplorePointOfUse.redeemType]
233248
category = .merchant(Merchant(merchantId: merchantId, paymentMethod: Merchant.PaymentMethod(rawValue: paymentMethodRaw)!,
234-
type: type, deeplink: deeplink, savingsBasisPoints: savingsPercentage, denominationsType: denominationsType))
249+
type: type, deeplink: deeplink, savingsBasisPoints: savingsPercentage, denominationsType: denominationsType, denominations: [], redeemType: redeemType))
235250
} else if let manufacturer = try? row.get(ExplorePointOfUse.manufacturer) {
236251
let type: Atm.`Type`! = .init(rawValue: row[ExplorePointOfUse.type])
237252
category = .atm(Atm(manufacturer: manufacturer, type: type))

DashWallet/Sources/Models/Explore Dash/Services/CTXSpendAPI.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ enum CTXSpendError: Error, LocalizedError {
2727
case insufficientFunds
2828
case invalidMerchant
2929
case invalidAmount
30+
case merchantUnavailable
31+
case transactionRejected
32+
case purchaseLimitExceeded
33+
case serverError
3034
case customError(String)
3135
case unknown
3236
case paymentProcessingError(String)
@@ -44,11 +48,19 @@ enum CTXSpendError: Error, LocalizedError {
4448
case .tokenRefreshFailed:
4549
return NSLocalizedString("Your session expired", comment: "DashSpend")
4650
case .insufficientFunds:
47-
return NSLocalizedString("Insufficient funds to complete this purchase.", comment: "DashSpend")
51+
return NSLocalizedString("You do not have sufficient funds to complete this transaction", comment: "DashSpend")
4852
case .invalidMerchant:
4953
return NSLocalizedString("This merchant is currently unavailable.", comment: "DashSpend")
5054
case .invalidAmount:
5155
return NSLocalizedString("Invalid amount. Please check merchant limits.", comment: "DashSpend")
56+
case .merchantUnavailable:
57+
return NSLocalizedString("This merchant is currently unavailable. Please try again later or choose a different merchant.", comment: "DashSpend")
58+
case .transactionRejected:
59+
return NSLocalizedString("Your transaction was rejected. Please try again or contact support if the problem persists.", comment: "DashSpend")
60+
case .purchaseLimitExceeded:
61+
return NSLocalizedString("The purchase limits for this merchant have changed. Please contact CTX Support for more information.", comment: "DashSpend")
62+
case .serverError:
63+
return NSLocalizedString("Server error occurred. Please try again later.", comment: "DashSpend")
5264
case .customError(let message):
5365
return message
5466
case .unknown:
@@ -88,7 +100,13 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
88100
if target.path.contains("/api/verify") {
89101
throw CTXSpendError.invalidCode
90102
}
91-
throw CTXSpendError.unknown
103+
throw CTXSpendError.invalidAmount
104+
} catch HTTPClientError.statusCode(let r) where r.statusCode == 409 {
105+
throw CTXSpendError.transactionRejected
106+
} catch HTTPClientError.statusCode(let r) where r.statusCode == 422 {
107+
throw CTXSpendError.invalidAmount
108+
} catch HTTPClientError.statusCode(let r) where r.statusCode >= 500 {
109+
throw CTXSpendError.serverError
92110
} catch HTTPClientError.decoder {
93111
throw CTXSpendError.parsingError
94112
}

DashWallet/Sources/Models/Explore Dash/Services/CTXSpendService.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,35 +135,46 @@ class CTXSpendService: CTXSpendAPIAccessTokenProvider, CTXSpendTokenProvider, Ob
135135
switch response.statusCode {
136136
case 400:
137137
if let errorData = try? JSONDecoder().decode(CTXSpendAPIError.self, from: response.data) {
138-
// Check for limit error first
138+
// Check for limit errors first
139139
if let fiatAmountErrors = errorData.fields?.fiatAmount,
140-
let firstFiatError = fiatAmountErrors.first,
141-
(firstFiatError == "above threshold" || firstFiatError == "below threshold") {
142-
throw CTXSpendError.customError(NSLocalizedString("The purchase limits for this merchant have changed. Please contact CTX Support for more information.", comment: "DashSpend"))
140+
let firstFiatError = fiatAmountErrors.first {
141+
if firstFiatError == "above threshold" || firstFiatError == "below threshold" {
142+
throw CTXSpendError.purchaseLimitExceeded
143+
}
143144
}
144145

145146
if let firstError = errorData.errors.first {
146147
// Look for specific error messages
147148
let errorMessage = firstError.message.lowercased()
148149

149-
if errorMessage.contains("insufficient") || errorMessage.contains("funds") {
150+
if errorMessage.contains("insufficient") || errorMessage.contains("funds") || errorMessage.contains("balance") {
150151
throw CTXSpendError.insufficientFunds
152+
} else if errorMessage.contains("merchant") && (errorMessage.contains("unavailable") || errorMessage.contains("disabled") || errorMessage.contains("suspended")) {
153+
throw CTXSpendError.merchantUnavailable
151154
} else if errorMessage.contains("merchant") {
152155
throw CTXSpendError.invalidMerchant
153-
} else if errorMessage.contains("amount") || errorMessage.contains("value") {
156+
} else if errorMessage.contains("rejected") || errorMessage.contains("declined") {
157+
throw CTXSpendError.transactionRejected
158+
} else if errorMessage.contains("amount") || errorMessage.contains("value") || errorMessage.contains("limit") {
154159
throw CTXSpendError.invalidAmount
155160
}
156161

157162
// Custom error with the actual message from API
158-
throw NSError(domain: "CTXSpend", code: 400, userInfo: [NSLocalizedDescriptionKey: firstError.message])
163+
throw CTXSpendError.customError(firstError.message)
159164
}
160165
}
161166
case 401, 403:
162167
throw CTXSpendError.unauthorized
163168
case 404:
164169
throw CTXSpendError.invalidMerchant
170+
case 409:
171+
// Conflict - usually means duplicate transaction or similar
172+
throw CTXSpendError.transactionRejected
173+
case 422:
174+
// Unprocessable Entity - validation errors
175+
throw CTXSpendError.invalidAmount
165176
case 500...599:
166-
throw CTXSpendError.networkError
177+
throw CTXSpendError.serverError
167178
default:
168179
break
169180
}

DashWallet/Sources/Models/Explore Dash/Services/ExploreDatabaseSyncManager.swift

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,39 @@ extension ExploreDatabaseSyncManager {
126126
self?.syncState = .error(date, e)
127127
} else {
128128
try? data?.write(to: urlToSave)
129-
self?.exploreDatabaseLastSyncTimestamp = now
130-
self?.exploreDatabaseLastVersion = timeIntervalMillesecond / 1000
131-
self?.syncState = .synced(date)
132-
self?.unzipFile(at: urlToSave.path, password: checksum)
129+
130+
Task {
131+
do {
132+
try await self?.unzipFile(at: urlToSave.path, password: checksum)
133+
self?.exploreDatabaseLastSyncTimestamp = now
134+
self?.exploreDatabaseLastVersion = timeIntervalMillesecond / 1000
135+
self?.syncState = .synced(date)
136+
137+
NotificationCenter.default.post(name: ExploreDatabaseSyncManager.databaseHasBeenUpdatedNotification, object: nil)
138+
try? FileManager.default.removeItem(at: URL(fileURLWithPath: urlToSave.path))
139+
} catch {
140+
DSLogger.log("ExploreDash: failed to open DB archive: \(String(describing: error))")
141+
self?.syncState = .error(Date(), error)
142+
}
143+
}
133144
}
134145
}
135146
}
136147

137-
private func unzipFile(at path: String, password: String) {
138-
var error: NSError?
148+
private func unzipFile(at path: String, password: String) async throws {
139149
let urlToUnzip = self.getDocumentsDirectory()
140-
SSZipArchive.unzipFile(atPath: path, toDestination: urlToUnzip.path, preserveAttributes: true, overwrite: true,
141-
nestedZipLevel: 0, password: password, error: &error, delegate: nil,
142-
progressHandler: nil) { path, _, _ in
143-
NotificationCenter.default.post(name: ExploreDatabaseSyncManager.databaseHasBeenUpdatedNotification, object: nil)
144-
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
150+
151+
return try await withCheckedThrowingContinuation { continuation in
152+
SSZipArchive.unzipFile(atPath: path, toDestination: urlToUnzip.path, preserveAttributes: true, overwrite: true,
153+
nestedZipLevel: 0, password: password, error: nil, delegate: nil,
154+
progressHandler: nil) { path, success, error in
155+
if success {
156+
continuation.resume()
157+
} else {
158+
let errorToThrow = error ?? NSError(domain: "ExploreDatabaseSyncManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to unzip archive"])
159+
continuation.resume(throwing: errorToThrow)
160+
}
161+
}
145162
}
146163
}
147164

0 commit comments

Comments
 (0)