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
2 changes: 1 addition & 1 deletion DashSyncCurrentCommit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b6a82ef4d57754d29599b794df3274e4cd36e07a
fdf97505762702eb6290d71c61a93c8f6a57e3ba
58 changes: 35 additions & 23 deletions DashWallet.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
class CTXConstants {
static let baseURI = "https://spend.ctx.com/"
static let ctxGiftCardAgreementUrl = "https://ctx.com/gift-card-agreement/"
static let supportEmail = "support@ctx.com"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// Created by Andrei Ashikhmin
// Copyright © 2025 Dash Core Group. All rights reserved.
//
Expand All @@ -17,38 +17,104 @@

import Foundation

// Request Models
public struct LoginRequest: Codable {
// MARK: - Request Models

struct LoginRequest: Codable {
let email: String
}

public struct VerifyEmailRequest: Codable {
struct VerifyEmailRequest: Codable {
let email: String
let code: String
}

public struct RefreshTokenRequest: Codable {
let refreshToken: String
}

public struct PurchaseGiftCardRequest: Codable {
let cryptoCurrency: String
let fiatCurrency: String
let fiatAmount: String
let merchantId: String
}

// Response Models
public struct VerifyEmailResponse: Codable {
// MARK: - Response Models

struct VerifyEmailResponse: Codable {
let accessToken: String
let refreshToken: String
}

struct RefreshTokenResponse: Codable {
let accessToken: String
let refreshToken: String
}

public struct GiftCardResponse: Codable {
let giftCardId: String
let dashAmount: String
let dashTxUrl: String
let checkoutUrl: String
struct GiftCardResponse: Codable {
let id: String
let percentDiscount: String
let paymentCryptoAmount: String
let cardFiatAmount: String
let cardFiatCurrency: String
let paymentUrls: [String: String]
let paymentCryptoCurrency: String
let paymentCryptoNetwork: String
let paymentFiatCurrency: String
let userId: String
let merchantName: String
let userEmail: String
let created: String
let rate: String
let paymentFiatAmount: String
let status: String
let paymentId: String
}

struct MerchantResponse: Codable {
let id: String
let name: String
let logoUrl: String
let enabled: Bool
let savingsPercentage: Int
let denominationsType: String
let denominations: [String]
let cachedLocationCount: Int
let mapPinUrl: String
let type: String
let redeemType: String
let info: MerchantInfo
let cardImageUrl: String
let currency: String

var minimumCardPurchase: Double {
guard denominations.count >= 1, let min = Double(denominations[0]) else { return 0.0 }
return min
}

var maximumCardPurchase: Double {
guard denominations.count >= 2, let max = Double(denominations[1]) else { return 0.0 }
return max
}

var denominationType: DenominationType {
switch denominationsType {
case "min-max":
return .Range
default:
return .Fixed
}
}
}

struct MerchantInfo: Codable {
let terms: String
let description: String
let instructions: String
let intro: String
}

public struct MerchantResponse: Codable {
let savingsPercentage: Double
let minimumCardPurchase: Double
let maximumCardPurchase: Double
enum DenominationType: String, Codable {
case Range = "range"
case Fixed = "fixed"
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ extension ExplorePointOfUse {
let type: `Type`
let deeplink: String?
let savingsBasisPoints: Int // in basis points 1 = 0.001%

func toSavingPercentages() -> Double {
return Double(savingsBasisPoints) / 100
}

func toSavingsFraction() -> Double {
return Double(savingsBasisPoints) / 10000
}
}

var merchant: Merchant? {
Expand Down
69 changes: 66 additions & 3 deletions DashWallet/Sources/Models/Explore Dash/Services/CTXSpendAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,50 @@
import Foundation
import Moya

enum CTXSpendError: Error {
enum CTXSpendError: Error, LocalizedError {
case networkError
case parsingError
case invalidCode
case unauthorized
case tokenRefreshFailed
case insufficientFunds
case invalidMerchant
case invalidAmount
case customError(String)
case unknown
case paymentProcessingError(String)

public var errorDescription: String? {
switch self {
case .networkError:
return NSLocalizedString("Network error. Please check your connection and try again.", comment: "DashSpend")
case .parsingError:
return NSLocalizedString("Error processing server response. Please try again later.", comment: "DashSpend")
case .invalidCode:
return NSLocalizedString("Invalid verification code. Please try again.", comment: "CTXSpend error")
case .unauthorized:
return NSLocalizedString("Please sign in to your DashSpend account.", comment: "DashSpend")
case .tokenRefreshFailed:
return NSLocalizedString("Your session expired", comment: "DashSpend")
case .insufficientFunds:
return NSLocalizedString("Insufficient funds to complete this purchase.", comment: "DashSpend")
case .invalidMerchant:
return NSLocalizedString("This merchant is currently unavailable.", comment: "DashSpend")
case .invalidAmount:
return NSLocalizedString("Invalid amount. Please check merchant limits.", comment: "DashSpend")
case .customError(let message):
return message
case .unknown:
return NSLocalizedString("An unknown error occurred. Please try again later.", comment: "DashSpend")
case .paymentProcessingError(let details):
return String(format: NSLocalizedString("Payment processing error: %@", comment: "DashSpend"), details)
}
}
}

protocol CTXSpendAPIAccessTokenProvider: AnyObject {
var accessToken: String? { get }
var refreshToken: String? { get }
}

final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
Expand All @@ -38,7 +72,8 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
try checkAccessTokenIfNeeded(for: target)
try await super.request(target)
} catch HTTPClientError.statusCode(let r) where r.statusCode == 401 {
throw CTXSpendError.unauthorized
try await handleUnauthorizedError(for: target)
try await super.request(target)
}
}

Expand All @@ -47,7 +82,8 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
try checkAccessTokenIfNeeded(for: target)
return try await super.request(target)
} catch HTTPClientError.statusCode(let r) where r.statusCode == 401 {
throw CTXSpendError.unauthorized
try await handleUnauthorizedError(for: target)
return try await super.request(target)
} catch HTTPClientError.statusCode(let r) where r.statusCode == 400 {
if target.path.contains("/api/verify") {
throw CTXSpendError.invalidCode
Expand All @@ -58,6 +94,28 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
}
}

// Direct request method that bypasses refresh logic (used by token service)
func requestDirectly<R>(_ target: CTXSpendEndpoint) async throws -> R where R: Decodable {
return try await super.request(target)
}

func requestDirectly(_ target: CTXSpendEndpoint) async throws {
try await super.request(target)
}

private func handleUnauthorizedError(for target: CTXSpendEndpoint) async throws {
guard target.authorizationType == .bearer else {
throw CTXSpendError.unauthorized
}

try await CTXSpendTokenService.shared.refreshAccessToken()

// Update the access token provider after refresh
accessTokenProvider = { [weak self] in
self?.ctxSpendAPIAccessTokenProvider?.accessToken
}
}

private func checkAccessTokenIfNeeded(for target: CTXSpendEndpoint) throws {
guard target.authorizationType == .bearer else {
return
Expand All @@ -79,5 +137,10 @@ final class CTXSpendAPI: HTTPClient<CTXSpendEndpoint> {
ctxSpendAPIAccessTokenProvider!.accessToken
}
self.ctxSpendAPIAccessTokenProvider = ctxSpendAPIAccessTokenProvider

// Configure the token service
if let tokenProvider = ctxSpendAPIAccessTokenProvider as? CTXSpendTokenProvider {
CTXSpendTokenService.shared.configure(with: tokenProvider)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,20 @@ struct CTXSpendAPIError: Decodable {
}
}

struct FieldError: Decodable {
let fiatAmount: [String]?
}

var errors: [Error]
let fields: FieldError?
}

// MARK: - CTXSpendEndpoint

public enum CTXSpendEndpoint {
case login(email: String)
case verifyEmail(email: String, code: String)
case refreshToken(RefreshTokenRequest)
case purchaseGiftCard(PurchaseGiftCardRequest)
case getMerchant(String)
case getGiftCard(String)
Expand All @@ -50,7 +56,7 @@ public enum CTXSpendEndpoint {
extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {
public var authorizationType: Moya.AuthorizationType? {
switch self {
case .login, .verifyEmail:
case .login, .verifyEmail, .refreshToken:
return nil
default:
return .bearer
Expand All @@ -65,6 +71,7 @@ extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {
switch self {
case .login: return "login"
case .verifyEmail: return "verify-email"
case .refreshToken: return "refresh-token"
case .purchaseGiftCard: return "gift-cards"
case .getMerchant(let merchantId): return "merchants/\(merchantId)"
case .getGiftCard(let txid): return "gift-cards"
Expand All @@ -73,7 +80,7 @@ extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {

public var method: Moya.Method {
switch self {
case .login, .verifyEmail, .purchaseGiftCard:
case .login, .verifyEmail, .refreshToken, .purchaseGiftCard:
return .post
default:
return .get
Expand All @@ -88,6 +95,8 @@ extension CTXSpendEndpoint: TargetType, AccessTokenAuthorizable {
case .verifyEmail(let email, let code):
let verifyRequest = VerifyEmailRequest(email: email, code: code)
return .requestJSONEncodable(verifyRequest)
case .refreshToken(let request):
return .requestJSONEncodable(request)
case .purchaseGiftCard(let request):
return .requestJSONEncodable(request)
default:
Expand Down
Loading