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 FlipcashAPI/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ enum ContractPackage: String, CaseIterable {
var version: Version {
switch self {
case .ocp: return "0.6.0"
case .flipcash2: return "0.13.0"
case .flipcash2: return "0.14.0"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ extension FlipClient {

/// Updates the caller's Tip Card customization. The server validates the
/// colour and falls back to the default for anything left unset.
public func updateTipCard(_ customization: TipCardCustomization, owner: KeyPair) async throws {
try await profileService.updateTipCard(color: customization.colorProto, owner: owner)
public func updateFlipcard(_ customization: TipCardCustomization, owner: KeyPair) async throws {
try await profileService.updateFlipcard(color: customization.colorProto, owner: owner)
}

/// Sets the minimum fee another user must pay to initialize a DM chat with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,27 +158,27 @@ final class ProfileService: Sendable {
}
}

func updateTipCard(color: Flipcash_Common_V1_Color, owner: KeyPair) async throws {
logger.info("Updating tip card")
func updateFlipcard(color: Flipcash_Common_V1_Color, owner: KeyPair) async throws {
logger.info("Updating flipcard")

var request = Flipcash_Profile_V1_UpdateTipCardRequest()
var request = Flipcash_Profile_V1_UpdateFlipcardRequest()
request.color = color
request.auth = owner.authFor(message: request)

do {
let response = try await service.updateTipCard(request, options: .unaryDefault)
let error = ErrorUpdateTipCard(rawValue: response.result.rawValue) ?? .unknown
let response = try await service.updateFlipcard(request, options: .unaryDefault)
let error = ErrorUpdateFlipcard(rawValue: response.result.rawValue) ?? .unknown
guard error == .ok else {
logger.error("Failed to update tip card", metadata: ["error": "\(error)"])
logger.error("Failed to update flipcard", metadata: ["error": "\(error)"])
throw error
}
logger.info("Tip card updated")
} catch let error as ErrorUpdateTipCard {
logger.info("Flipcard updated")
} catch let error as ErrorUpdateFlipcard {
throw error
} catch let error as RPCError {
throw ErrorUpdateTipCard.from(transportError: error)
throw ErrorUpdateFlipcard.from(transportError: error)
} catch {
throw ErrorUpdateTipCard.unknown
throw ErrorUpdateFlipcard.unknown
}
}

Expand Down Expand Up @@ -238,7 +238,7 @@ extension ErrorFetchProfile: ServerError, TransportClassifiableError {
}
}

public enum ErrorUpdateTipCard: Int, Error {
public enum ErrorUpdateFlipcard: Int, Error {
case ok
case denied
/// The requested colour failed server-side validation (bad hex).
Expand All @@ -249,7 +249,7 @@ public enum ErrorUpdateTipCard: Int, Error {
case rejected = -4
}

extension ErrorUpdateTipCard: ServerError, TransportClassifiableError {
extension ErrorUpdateFlipcard: ServerError, TransportClassifiableError {
public var reportingLevel: ErrorReportingLevel {
switch self {
case .ok, .transportFailure: .suppressed
Expand Down
37 changes: 31 additions & 6 deletions FlipcashCore/Sources/FlipcashCore/Models/Profile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public struct Profile: Codable, Equatable, Sendable {
/// the user hasn't set one, in which case the server default applies.
public let minDmChatInitFee: FiatAmount?

/// Whether the current username was assigned by the server from the display
/// name rather than chosen with `SetUsername`. Private: the server sets it
/// only on the caller's own profile, so it is `false` on anyone else's and
/// when there is no username.
public let isUsernameAutoAssigned: Bool

public var isPhoneVerified: Bool {
phone != nil
}
Expand All @@ -61,7 +67,7 @@ public struct Profile: Codable, Equatable, Sendable {
phone != nil && phone?.e164 != previous?.phone?.e164
}

public init(displayName: String?, phone: String?, email: String?, profilePicture: ProfilePicture? = nil, joinedAt: Date? = nil, tipCardCustomization: TipCardCustomization? = nil, userID: UserID? = nil, username: Username? = nil, minDmChatInitFee: FiatAmount? = nil) throws {
public init(displayName: String?, phone: String?, email: String?, profilePicture: ProfilePicture? = nil, joinedAt: Date? = nil, tipCardCustomization: TipCardCustomization? = nil, userID: UserID? = nil, username: Username? = nil, minDmChatInitFee: FiatAmount? = nil, isUsernameAutoAssigned: Bool = false) throws {

// Only parse phone if it's not empty
var parsedPhone: Phone?
Expand All @@ -86,11 +92,12 @@ public struct Profile: Codable, Equatable, Sendable {
tipCardCustomization: tipCardCustomization,
userID: userID,
username: username,
minDmChatInitFee: minDmChatInitFee
minDmChatInitFee: minDmChatInitFee,
isUsernameAutoAssigned: isUsernameAutoAssigned
)
}

public init(displayName: String?, phone: Phone?, email: String?, profilePicture: ProfilePicture? = nil, joinedAt: Date? = nil, tipCardCustomization: TipCardCustomization? = nil, userID: UserID? = nil, username: Username? = nil, minDmChatInitFee: FiatAmount? = nil) {
public init(displayName: String?, phone: Phone?, email: String?, profilePicture: ProfilePicture? = nil, joinedAt: Date? = nil, tipCardCustomization: TipCardCustomization? = nil, userID: UserID? = nil, username: Username? = nil, minDmChatInitFee: FiatAmount? = nil, isUsernameAutoAssigned: Bool = false) {
self.displayName = displayName
self.phone = phone
self.email = email
Expand All @@ -100,6 +107,23 @@ public struct Profile: Codable, Equatable, Sendable {
self.userID = userID
self.username = username
self.minDmChatInitFee = minDmChatInitFee
self.isUsernameAutoAssigned = isUsernameAutoAssigned
}

/// `isUsernameAutoAssigned` is decoded with a default so rows persisted
/// before it existed still decode, without a `schemaVersion` bump.
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.displayName = try c.decodeIfPresent(String.self, forKey: .displayName)
self.phone = try c.decodeIfPresent(Phone.self, forKey: .phone)
self.email = try c.decodeIfPresent(String.self, forKey: .email)
self.profilePicture = try c.decodeIfPresent(ProfilePicture.self, forKey: .profilePicture)
self.joinedAt = try c.decodeIfPresent(Date.self, forKey: .joinedAt)
self.tipCardCustomization = try c.decodeIfPresent(TipCardCustomization.self, forKey: .tipCardCustomization)
self.userID = try c.decodeIfPresent(UserID.self, forKey: .userID)
self.username = try c.decodeIfPresent(Username.self, forKey: .username)
self.minDmChatInitFee = try c.decodeIfPresent(FiatAmount.self, forKey: .minDmChatInitFee)
self.isUsernameAutoAssigned = try c.decodeIfPresent(Bool.self, forKey: .isUsernameAutoAssigned) ?? false
}
}

Expand Down Expand Up @@ -131,19 +155,20 @@ extension Profile {
email: proto.emailAddress.value,
profilePicture: proto.hasProfilePicture ? ProfilePicture(proto.profilePicture) : nil,
joinedAt: proto.hasJoinTs ? proto.joinTs.date : nil,
tipCardCustomization: proto.hasTipCardCustomization ? TipCardCustomization(proto.tipCardCustomization) : nil,
tipCardCustomization: proto.hasFlipcardCustomization ? TipCardCustomization(proto.flipcardCustomization) : nil,
userID: proto.hasUserID ? try? UUID(data: proto.userID.value) : nil,
username: proto.hasUsername ? Username(proto.username) : nil,
minDmChatInitFee: proto.hasMinDmChatInitFee ? FiatAmount(
value: Decimal(proto.minDmChatInitFee.nativeAmount),
currency: try CurrencyCode(currencyCode: proto.minDmChatInitFee.currency)
) : nil
) : nil,
isUsernameAutoAssigned: proto.isUsernameAutoAssigned
)
}
}

extension TipCardCustomization {
init(_ proto: Flipcash_Profile_V1_TipCardCustomization) {
init(_ proto: Flipcash_Profile_V1_FlipcardCustomization) {
self.init(colorHex: proto.color.hex)
}

Expand Down
4 changes: 2 additions & 2 deletions FlipcashCore/Sources/FlipcashCore/Models/UserFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ extension UserFlags {
enablePhoneNumberSend: proto.enablePhoneNumberSend,
requireCoinbaseEmailVerification: proto.requireCoinbaseEmailVerification,
preferredOnrampUsdcLiquidityPool: UsdcLiquidityPool(proto.preferredOnRampUsdcLiquidityPool),
tipPresets: proto.tipPresets.compactMap { TipPresets($0) },
tipPresets: proto.sendPresets.compactMap { TipPresets($0) },
messageEditWindow: proto.hasMessageEditWindow ? TimeInterval(proto.messageEditWindow.seconds) : nil,
messageDeleteWindow: proto.hasMessageDeleteWindow ? TimeInterval(proto.messageDeleteWindow.seconds) : nil
)
Expand All @@ -165,7 +165,7 @@ extension UserFlags {
extension UserFlags.TipPresets {

/// Returns nil for a region code this client doesn't recognize.
init?(_ proto: Flipcash_Account_V1_TipPresets) {
init?(_ proto: Flipcash_Account_V1_SendPresets) {
guard let currency = CurrencyCode(rawValue: proto.region.value) else {
return nil
}
Expand Down
24 changes: 24 additions & 0 deletions FlipcashCore/Tests/FlipcashCoreTests/ProfileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,30 @@ struct ProfileTests {
#expect(profile.tipCardCustomization == nil)
}

// MARK: - Auto-assigned username -

@Test("isUsernameAutoAssigned maps from the proto and round-trips")
func autoAssignedFlagMapsAndRoundTrips() throws {
var proto = Flipcash_Profile_V1_UserProfile()
proto.isUsernameAutoAssigned = true

let profile = try Profile(proto)
#expect(profile.isUsernameAutoAssigned)

let restored = try JSONDecoder().decode(Profile.self, from: try JSONEncoder().encode(profile))
#expect(restored.isUsernameAutoAssigned)
}

@Test("A row persisted before isUsernameAutoAssigned decodes it as false")
func decodesProfilePersistedBeforeAutoAssignedFlag() throws {
let legacy = Data(#"{"displayName":"Ted Livingston","email":"ted@example.com"}"#.utf8)

let profile = try JSONDecoder().decode(Profile.self, from: legacy)

#expect(profile.isUsernameAutoAssigned == false)
#expect(try Profile(Flipcash_Profile_V1_UserProfile()).isUsernameAutoAssigned == false)
}

// MARK: - Username -

@Test("Username round-trips")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct TransportClassificationTests {
@Test func errorCheckEmailCode() { assertClassifies(ErrorCheckEmailCode.self) }
@Test func errorUnlinkEmail() { assertClassifies(ErrorUnlinkEmail.self) }
@Test func errorFetchProfile() { assertClassifies(ErrorFetchProfile.self) }
@Test func errorUpdateTipCard() { assertClassifies(ErrorUpdateTipCard.self) }
@Test func errorUpdateFlipcard() { assertClassifies(ErrorUpdateFlipcard.self) }
@Test func errorSetMinDmChatInitFee() { assertClassifies(ErrorSetMinDmChatInitFee.self) }
@Test func errorBlocklist() { assertClassifies(ErrorBlocklist.self) }
@Test func errorRateHistory() { assertClassifies(ErrorRateHistory.self) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct UserFlagsTipPresetsTests {
_ rows: [(region: String, minimum: Double, low: Double, medium: Double, high: Double)]
) -> Flipcash_Account_V1_UserFlags {
.with {
$0.tipPresets = rows.map { row in
$0.sendPresets = rows.map { row in
.with {
$0.region = .with { $0.value = row.region }
$0.minimum = row.minimum
Expand Down
Loading