Skip to content

Commit eaf7b1f

Browse files
authored
Merge pull request #705 from Syn-McJ/feat/dashspend-buy-backend
feat: backend functions for buy giftcard
2 parents c6b86e9 + 1b6787d commit eaf7b1f

66 files changed

Lines changed: 3451 additions & 427 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.

DashSyncCurrentCommit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
b6a82ef4d57754d29599b794df3274e4cd36e07a
1+
fdf97505762702eb6290d71c61a93c8f6a57e3ba

DashWallet.xcodeproj/project.pbxproj

Lines changed: 35 additions & 23 deletions
Large diffs are not rendered by default.

DashWallet/Sources/Models/CrowdNode/Services/SendCoinsService.swift

Lines changed: 0 additions & 66 deletions
This file was deleted.

DashWallet/Sources/Models/Explore Dash/Model/CTXConstants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
class CTXConstants {
1919
static let baseURI = "https://spend.ctx.com/"
2020
static let ctxGiftCardAgreementUrl = "https://ctx.com/gift-card-agreement/"
21+
static let supportEmail = "support@ctx.com"
2122
}
Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//
1+
//
22
// Created by Andrei Ashikhmin
33
// Copyright © 2025 Dash Core Group. All rights reserved.
44
//
@@ -17,38 +17,104 @@
1717

1818
import Foundation
1919

20-
// Request Models
21-
public struct LoginRequest: Codable {
20+
// MARK: - Request Models
21+
22+
struct LoginRequest: Codable {
2223
let email: String
2324
}
2425

25-
public struct VerifyEmailRequest: Codable {
26+
struct VerifyEmailRequest: Codable {
2627
let email: String
2728
let code: String
2829
}
2930

31+
public struct RefreshTokenRequest: Codable {
32+
let refreshToken: String
33+
}
34+
3035
public struct PurchaseGiftCardRequest: Codable {
3136
let cryptoCurrency: String
3237
let fiatCurrency: String
3338
let fiatAmount: String
3439
let merchantId: String
3540
}
3641

37-
// Response Models
38-
public struct VerifyEmailResponse: Codable {
42+
// MARK: - Response Models
43+
44+
struct VerifyEmailResponse: Codable {
45+
let accessToken: String
46+
let refreshToken: String
47+
}
48+
49+
struct RefreshTokenResponse: Codable {
3950
let accessToken: String
4051
let refreshToken: String
4152
}
4253

43-
public struct GiftCardResponse: Codable {
44-
let giftCardId: String
45-
let dashAmount: String
46-
let dashTxUrl: String
47-
let checkoutUrl: String
54+
struct GiftCardResponse: Codable {
55+
let id: String
56+
let percentDiscount: String
57+
let paymentCryptoAmount: String
58+
let cardFiatAmount: String
59+
let cardFiatCurrency: String
60+
let paymentUrls: [String: String]
61+
let paymentCryptoCurrency: String
62+
let paymentCryptoNetwork: String
63+
let paymentFiatCurrency: String
64+
let userId: String
65+
let merchantName: String
66+
let userEmail: String
67+
let created: String
68+
let rate: String
69+
let paymentFiatAmount: String
70+
let status: String
71+
let paymentId: String
72+
}
73+
74+
struct MerchantResponse: Codable {
75+
let id: String
76+
let name: String
77+
let logoUrl: String
78+
let enabled: Bool
79+
let savingsPercentage: Int
80+
let denominationsType: String
81+
let denominations: [String]
82+
let cachedLocationCount: Int
83+
let mapPinUrl: String
84+
let type: String
85+
let redeemType: String
86+
let info: MerchantInfo
87+
let cardImageUrl: String
88+
let currency: String
89+
90+
var minimumCardPurchase: Double {
91+
guard denominations.count >= 1, let min = Double(denominations[0]) else { return 0.0 }
92+
return min
93+
}
94+
95+
var maximumCardPurchase: Double {
96+
guard denominations.count >= 2, let max = Double(denominations[1]) else { return 0.0 }
97+
return max
98+
}
99+
100+
var denominationType: DenominationType {
101+
switch denominationsType {
102+
case "min-max":
103+
return .Range
104+
default:
105+
return .Fixed
106+
}
107+
}
108+
}
109+
110+
struct MerchantInfo: Codable {
111+
let terms: String
112+
let description: String
113+
let instructions: String
114+
let intro: String
48115
}
49116

50-
public struct MerchantResponse: Codable {
51-
let savingsPercentage: Double
52-
let minimumCardPurchase: Double
53-
let maximumCardPurchase: Double
117+
enum DenominationType: String, Codable {
118+
case Range = "range"
119+
case Fixed = "fixed"
54120
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ extension ExplorePointOfUse {
5858
let type: `Type`
5959
let deeplink: String?
6060
let savingsBasisPoints: Int // in basis points 1 = 0.001%
61+
62+
func toSavingPercentages() -> Double {
63+
return Double(savingsBasisPoints) / 100
64+
}
65+
66+
func toSavingsFraction() -> Double {
67+
return Double(savingsBasisPoints) / 10000
68+
}
6169
}
6270

6371
var merchant: Merchant? {

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

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,50 @@
1818
import Foundation
1919
import Moya
2020

21-
enum CTXSpendError: Error {
21+
enum CTXSpendError: Error, LocalizedError {
2222
case networkError
2323
case parsingError
2424
case invalidCode
2525
case unauthorized
26+
case tokenRefreshFailed
27+
case insufficientFunds
28+
case invalidMerchant
29+
case invalidAmount
30+
case customError(String)
2631
case unknown
32+
case paymentProcessingError(String)
33+
34+
public var errorDescription: String? {
35+
switch self {
36+
case .networkError:
37+
return NSLocalizedString("Network error. Please check your connection and try again.", comment: "DashSpend")
38+
case .parsingError:
39+
return NSLocalizedString("Error processing server response. Please try again later.", comment: "DashSpend")
40+
case .invalidCode:
41+
return NSLocalizedString("Invalid verification code. Please try again.", comment: "CTXSpend error")
42+
case .unauthorized:
43+
return NSLocalizedString("Please sign in to your DashSpend account.", comment: "DashSpend")
44+
case .tokenRefreshFailed:
45+
return NSLocalizedString("Your session expired", comment: "DashSpend")
46+
case .insufficientFunds:
47+
return NSLocalizedString("Insufficient funds to complete this purchase.", comment: "DashSpend")
48+
case .invalidMerchant:
49+
return NSLocalizedString("This merchant is currently unavailable.", comment: "DashSpend")
50+
case .invalidAmount:
51+
return NSLocalizedString("Invalid amount. Please check merchant limits.", comment: "DashSpend")
52+
case .customError(let message):
53+
return message
54+
case .unknown:
55+
return NSLocalizedString("An unknown error occurred. Please try again later.", comment: "DashSpend")
56+
case .paymentProcessingError(let details):
57+
return String(format: NSLocalizedString("Payment processing error: %@", comment: "DashSpend"), details)
58+
}
59+
}
2760
}
2861

2962
protocol CTXSpendAPIAccessTokenProvider: AnyObject {
3063
var accessToken: String? { get }
64+
var refreshToken: String? { get }
3165
}
3266

3367
final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
@@ -38,7 +72,8 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
3872
try checkAccessTokenIfNeeded(for: target)
3973
try await super.request(target)
4074
} catch HTTPClientError.statusCode(let r) where r.statusCode == 401 {
41-
throw CTXSpendError.unauthorized
75+
try await handleUnauthorizedError(for: target)
76+
try await super.request(target)
4277
}
4378
}
4479

@@ -47,7 +82,8 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
4782
try checkAccessTokenIfNeeded(for: target)
4883
return try await super.request(target)
4984
} catch HTTPClientError.statusCode(let r) where r.statusCode == 401 {
50-
throw CTXSpendError.unauthorized
85+
try await handleUnauthorizedError(for: target)
86+
return try await super.request(target)
5187
} catch HTTPClientError.statusCode(let r) where r.statusCode == 400 {
5288
if target.path.contains("/api/verify") {
5389
throw CTXSpendError.invalidCode
@@ -58,6 +94,28 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
5894
}
5995
}
6096

97+
// Direct request method that bypasses refresh logic (used by token service)
98+
func requestDirectly<R>(_ target: CTXSpendEndpoint) async throws -> R where R: Decodable {
99+
return try await super.request(target)
100+
}
101+
102+
func requestDirectly(_ target: CTXSpendEndpoint) async throws {
103+
try await super.request(target)
104+
}
105+
106+
private func handleUnauthorizedError(for target: CTXSpendEndpoint) async throws {
107+
guard target.authorizationType == .bearer else {
108+
throw CTXSpendError.unauthorized
109+
}
110+
111+
try await CTXSpendTokenService.shared.refreshAccessToken()
112+
113+
// Update the access token provider after refresh
114+
accessTokenProvider = { [weak self] in
115+
self?.ctxSpendAPIAccessTokenProvider?.accessToken
116+
}
117+
}
118+
61119
private func checkAccessTokenIfNeeded(for target: CTXSpendEndpoint) throws {
62120
guard target.authorizationType == .bearer else {
63121
return
@@ -79,5 +137,10 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
79137
ctxSpendAPIAccessTokenProvider!.accessToken
80138
}
81139
self.ctxSpendAPIAccessTokenProvider = ctxSpendAPIAccessTokenProvider
140+
141+
// Configure the token service
142+
if let tokenProvider = ctxSpendAPIAccessTokenProvider as? CTXSpendTokenProvider {
143+
CTXSpendTokenService.shared.configure(with: tokenProvider)
144+
}
82145
}
83146
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,20 @@ struct CTXSpendAPIError: Decodable {
3232
}
3333
}
3434

35+
struct FieldError: Decodable {
36+
let fiatAmount: [String]?
37+
}
38+
3539
var errors: [Error]
40+
let fields: FieldError?
3641
}
3742

3843
// MARK: - CTXSpendEndpoint
3944

4045
public enum CTXSpendEndpoint {
4146
case login(email: String)
4247
case verifyEmail(email: String, code: String)
48+
case refreshToken(RefreshTokenRequest)
4349
case purchaseGiftCard(PurchaseGiftCardRequest)
4450
case getMerchant(String)
4551
case getGiftCard(String)
@@ -50,7 +56,7 @@ public enum CTXSpendEndpoint {
5056
extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {
5157
public var authorizationType: Moya.AuthorizationType? {
5258
switch self {
53-
case .login, .verifyEmail:
59+
case .login, .verifyEmail, .refreshToken:
5460
return nil
5561
default:
5662
return .bearer
@@ -65,6 +71,7 @@ extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {
6571
switch self {
6672
case .login: return "login"
6773
case .verifyEmail: return "verify-email"
74+
case .refreshToken: return "refresh-token"
6875
case .purchaseGiftCard: return "gift-cards"
6976
case .getMerchant(let merchantId): return "merchants/\(merchantId)"
7077
case .getGiftCard(let txid): return "gift-cards"
@@ -73,7 +80,7 @@ extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {
7380

7481
public var method: Moya.Method {
7582
switch self {
76-
case .login, .verifyEmail, .purchaseGiftCard:
83+
case .login, .verifyEmail, .refreshToken, .purchaseGiftCard:
7784
return .post
7885
default:
7986
return .get
@@ -88,6 +95,8 @@ extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {
8895
case .verifyEmail(let email, let code):
8996
let verifyRequest = VerifyEmailRequest(email: email, code: code)
9097
return .requestJSONEncodable(verifyRequest)
98+
case .refreshToken(let request):
99+
return .requestJSONEncodable(request)
91100
case .purchaseGiftCard(let request):
92101
return .requestJSONEncodable(request)
93102
default:

0 commit comments

Comments
 (0)