diff --git a/FlipcashAPI/Package.swift b/FlipcashAPI/Package.swift index 2f5d7a29f..e185ac533 100644 --- a/FlipcashAPI/Package.swift +++ b/FlipcashAPI/Package.swift @@ -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" } } diff --git a/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/FlipClient+Profile.swift b/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/FlipClient+Profile.swift index 56febfac0..76097d5ec 100644 --- a/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/FlipClient+Profile.swift +++ b/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/FlipClient+Profile.swift @@ -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 diff --git a/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/Services/ProfileService.swift b/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/Services/ProfileService.swift index 3708598ed..3d3dba40c 100644 --- a/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/Services/ProfileService.swift +++ b/FlipcashCore/Sources/FlipcashCore/Clients/Flip API/Services/ProfileService.swift @@ -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 } } @@ -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). @@ -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 diff --git a/FlipcashCore/Sources/FlipcashCore/Models/Profile.swift b/FlipcashCore/Sources/FlipcashCore/Models/Profile.swift index d86c398f1..01ab69af4 100644 --- a/FlipcashCore/Sources/FlipcashCore/Models/Profile.swift +++ b/FlipcashCore/Sources/FlipcashCore/Models/Profile.swift @@ -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 } @@ -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? @@ -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 @@ -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 } } @@ -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) } diff --git a/FlipcashCore/Sources/FlipcashCore/Models/UserFlags.swift b/FlipcashCore/Sources/FlipcashCore/Models/UserFlags.swift index 987c969ef..f75f44d85 100644 --- a/FlipcashCore/Sources/FlipcashCore/Models/UserFlags.swift +++ b/FlipcashCore/Sources/FlipcashCore/Models/UserFlags.swift @@ -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 ) @@ -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 } diff --git a/FlipcashCore/Tests/FlipcashCoreTests/ProfileTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/ProfileTests.swift index 1893b71a5..77c94cea7 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/ProfileTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/ProfileTests.swift @@ -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") diff --git a/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift index 048a607fb..1b08d7882 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/TransportClassificationTests.swift @@ -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) } diff --git a/FlipcashCore/Tests/FlipcashCoreTests/UserFlagsTipPresetsTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/UserFlagsTipPresetsTests.swift index 30e4f7fb1..b840d9f9f 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/UserFlagsTipPresetsTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/UserFlagsTipPresetsTests.swift @@ -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