From aac77014e5282234248176df2c56e43802feb947 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 10 Aug 2026 19:05:54 -0400 Subject: [PATCH 1/4] feat(protos): refresh flipcash protobufs and support new fields + UpdateTipCard Re-vendors the flipcash proto definitions and wires up the Kotlin service layer. Contract refactor (types consolidated into common/v1): - PhoneNumber, EmailAddress and Substitution moved to common/v1; updated all generated-type references (api, extensions, models). Substitution.KindCase.CONTACT became PHONE_NUMBER_TO_CONTACT_NAME and gained USER_ID_TO_DISPLAY_NAME. New capabilities supported at the domain/mapper level: - profile: UserProfile.tipCardColor (from TipCardCustomization.color.hex) and a new UpdateTipCard RPC scaffolded across api/service/repository/controller + errors. - chat: ChatType.GROUP and ChatMetadata.title. - activity: DirectlySentCrypto/ReceivedCrypto now carry an optional userId (the metadata oneof gained a user_id option), plus Notification.textSubstitutions; added a Substitution.UserId domain variant. Tests updated for the ChatType.GROUP addition, the relocated proto DSL builders, and the UpdateTipCard repository fake. --- .../app/core/feed/ActivityFeedMessage.kt | 2 + .../com/flipcash/app/analytics/Events.kt | 1 + .../shared/chat/internal/ChatIdGenerator.kt | 1 + .../app/notifications/NotificationService.kt | 5 ++ .../NotificationToEntityMapper.kt | 4 +- .../src/main/proto/activity/v1/model.proto | 10 ++-- .../protos/src/main/proto/chat/v1/model.proto | 9 ++++ .../src/main/proto/common/v1/common.proto | 46 ++++++++++++++++++- .../contact/v1/contact_list_service.proto | 7 ++- .../src/main/proto/contact/v1/model.proto | 3 +- .../email/v1/email_verification_service.proto | 6 +-- .../src/main/proto/email/v1/model.proto | 5 -- .../src/main/proto/intent/v1/model.proto | 5 +- .../src/main/proto/phone/v1/model.proto | 6 --- .../phone/v1/phone_verification_service.proto | 8 ++-- .../src/main/proto/profile/v1/model.proto | 19 ++++++-- .../proto/profile/v1/profile_service.proto | 20 ++++++++ .../protos/src/main/proto/push/v1/model.proto | 23 ++-------- .../src/main/proto/resolver/v1/model.proto | 5 +- .../services/controllers/ProfileController.kt | 10 ++++ .../domain/ActivityFeedMessageMapper.kt | 32 ++++++++++--- .../internal/domain/ChatMetadataMapper.kt | 1 + .../internal/domain/UserProfileMapper.kt | 1 + .../internal/network/api/ContactListApi.kt | 8 ++-- .../network/api/EmailVerificationApi.kt | 7 +-- .../network/api/PhoneVerificationApi.kt | 8 ++-- .../internal/network/api/ProfileApi.kt | 24 ++++++++++ .../internal/network/api/ResolverApi.kt | 4 +- .../network/extensions/LocalToProtobuf.kt | 1 + .../network/extensions/ProtobufToLocal.kt | 13 ++++-- .../network/services/ProfileService.kt | 21 +++++++++ .../repositories/InternalProfileRepository.kt | 8 ++++ .../models/ActivityFeedNotification.kt | 5 +- .../services/models/DmPaymentMetadata.kt | 5 +- .../com/flipcash/services/models/Errors.kt | 10 ++++ .../services/models/NotificationPayload.kt | 5 ++ .../flipcash/services/models/UserProfile.kt | 3 ++ .../services/models/chat/ChatMetadata.kt | 2 + .../flipcash/services/models/chat/ChatType.kt | 1 + .../services/repository/ProfileRepository.kt | 1 + .../controllers/ProfileControllerTest.kt | 2 + .../internal/domain/UserProfileMapperTest.kt | 4 +- .../services/models/chat/DomainModelsTest.kt | 3 +- 43 files changed, 274 insertions(+), 90 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/feed/ActivityFeedMessage.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/feed/ActivityFeedMessage.kt index 9038dfb45..e85b8b6b2 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/feed/ActivityFeedMessage.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/feed/ActivityFeedMessage.kt @@ -55,6 +55,7 @@ sealed interface MessageMetadata { @Serializable data class DirectlySentCrypto( val phoneNumber: String? = null, + val userId: ID? = null, ) : MessageMetadata @Serializable @@ -66,6 +67,7 @@ sealed interface MessageMetadata { @Serializable data class ReceivedCrypto( val phoneNumber: String? = null, + val userId: ID? = null, ) : MessageMetadata @Serializable diff --git a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt index 1f8cbe15f..6d55248fb 100644 --- a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt +++ b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt @@ -380,6 +380,7 @@ internal val ChatType.propertyValue: String get() = when (this) { ChatType.CONTACT_DM -> "Contact" ChatType.TIP_DM -> "Tip" + ChatType.GROUP -> "Group" ChatType.UNKNOWN -> "Unknown" } diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt index a34332111..240afcd54 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt @@ -25,6 +25,7 @@ class ChatIdGenerator @Inject constructor() { private fun ChatType.dmDomain(): String = when (this) { ChatType.CONTACT_DM -> DM_DOMAIN ChatType.TIP_DM -> TIP_DM_DOMAIN + ChatType.GROUP -> error("cannot derive a DM chat id for chat type $this") ChatType.UNKNOWN -> error("cannot derive a DM chat id for chat type $this") } diff --git a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt index 2173f94c5..d1af116b2 100644 --- a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt +++ b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt @@ -343,6 +343,11 @@ class NotificationService : FirebaseMessagingService(), is Substitution.Phone -> { contactResolver.resolveName(substitution.phoneNumber, substitution.fallback) } + is Substitution.UserId -> { + // Resolving a userId to a display name requires a network lookup not available here; + // degrade gracefully by using the server-provided fallback string. + substitution.fallback + } } } diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/notifications/NotificationToEntityMapper.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/notifications/NotificationToEntityMapper.kt index 6268b55ce..37c6a694f 100644 --- a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/notifications/NotificationToEntityMapper.kt +++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/notifications/NotificationToEntityMapper.kt @@ -55,8 +55,8 @@ class MetadataMapper @Inject constructor(): Mapper MessageMetadata.DirectlySentCrypto(from.phoneNumber) - is NotificationMetadata.ReceivedCrypto -> MessageMetadata.ReceivedCrypto(from.phoneNumber) + is NotificationMetadata.DirectlySentCrypto -> MessageMetadata.DirectlySentCrypto(from.phoneNumber, from.userId) + is NotificationMetadata.ReceivedCrypto -> MessageMetadata.ReceivedCrypto(from.phoneNumber, from.userId) is NotificationMetadata.IndirectlySentCrypto -> MessageMetadata.IndirectlySentCrypto(from.creator, from.canCancel) NotificationMetadata.Unknown -> MessageMetadata.Unknown NotificationMetadata.WithdrewCrypto -> MessageMetadata.WithdrewCrypto diff --git a/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto b/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto index 1f1cfe525..9622feb7d 100644 --- a/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/activity/v1/model.proto @@ -7,7 +7,6 @@ option java_package = "com.codeinc.flipcash.gen.activity.v1"; option objc_class_prefix = "FCPBActivityV1"; import "common/v1/common.proto"; -import "phone/v1/model.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -51,17 +50,22 @@ message Notification { } reserved 6; // Deprecated WelcomeBonusNotificationMetadata + + // Ordered substitutions to apply to localized_text + repeated common.v1.Substitution text_substitutions = 100; } message DirectlySentCryptoNotificationMetadata { oneof destination_identifier { - phone.v1.PhoneNumber phone = 1; + common.v1.PhoneNumber phone = 1; + common.v1.UserId user_id = 2; } } message ReceivedCryptoNotificationMetadata { oneof source_identifier { - phone.v1.PhoneNumber phone = 1; + common.v1.PhoneNumber phone = 1; + common.v1.UserId user_id = 2; } } diff --git a/definitions/flipcash/protos/src/main/proto/chat/v1/model.proto b/definitions/flipcash/protos/src/main/proto/chat/v1/model.proto index 45c6084a4..d654e684b 100644 --- a/definitions/flipcash/protos/src/main/proto/chat/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/chat/v1/model.proto @@ -16,6 +16,7 @@ enum ChatType { UNKNOWN = 0; CONTACT_DM = 1; TIP_DM = 2; + GROUP = 3; } message Metadata { @@ -27,6 +28,8 @@ message Metadata { }]; // Members of this chat + // + // For large group chats, this is a subset of all members. repeated Member members = 3; // The last message in this chat @@ -50,6 +53,12 @@ message Metadata { // Per-viewer and server-computed (e.g. the chat's peer is on the caller's // blocklist). Clients should exclude hidden chats from the primary DM list. bool is_hidden = 7; + + // Title for this chat. Only supported for group chats + string title = 8 [(validate.rules).string = { + min_len: 0 + max_len: 64 + }]; } message Member { diff --git a/definitions/flipcash/protos/src/main/proto/common/v1/common.proto b/definitions/flipcash/protos/src/main/proto/common/v1/common.proto index 719adbe51..73a1eafa5 100644 --- a/definitions/flipcash/protos/src/main/proto/common/v1/common.proto +++ b/definitions/flipcash/protos/src/main/proto/common/v1/common.proto @@ -6,6 +6,9 @@ option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/g option java_package = "com.codeinc.flipcash.gen.common.v1"; option objc_class_prefix = "FPBCommonV1"; +// Note: common/v1 is imported by every other domain, so it must remain a leaf +// package. Importing another flipcash domain here creates a Go import cycle, +// since that domain's service protos import common/v1 in turn. import "validate/validate.proto"; message PublicKey { @@ -59,8 +62,11 @@ message UserId { } message ChatId { + // value has the following structure: + // - 32 byte hash for DMs + // - 16 byte UUID for group chats bytes value = 1 [(validate.rules).bytes = { - min_len: 32 + min_len: 16 max_len: 32 }]; } @@ -82,6 +88,17 @@ message AppInstallId { }]; } +// PhoneNumber is an E.164 phone number +message PhoneNumber { + // Regex provided by Twilio here: https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 + string value = 1 [(validate.rules).string.pattern = "^\\+[1-9]\\d{1,14}$"]; +} + +// EmailAddress is an email address +message EmailAddress { + string value = 1 [(validate.rules).string.pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"]; +} + enum Platform { UNKNOWN = 0; APPLE = 1; @@ -182,3 +199,30 @@ message Region { pattern: "^[a-z]{3,4}$" }]; } + +// Color represents an RGB colour +message Color { + // Hex colour value (e.g. "#19191A") + string hex = 1 [(validate.rules).string = { + pattern: "^#[0-9a-fA-F]{6}$" + }]; +} + +// Substitution is a text subsitution +message Substitution { + // Fallback string for forwards compatibility + string fallback = 1 [(validate.rules).string = { + min_len: 1 + max_len: 4096 // Arbitrary + }]; + + oneof kind { + option (validate.required) = true; + + // Phone number -> contact name or formatted phone number + common.v1.PhoneNumber phone_number_to_contact_name = 2; + + // User ID -> display name + common.v1.UserId user_id_to_display_name = 3; + } +} diff --git a/definitions/flipcash/protos/src/main/proto/contact/v1/contact_list_service.proto b/definitions/flipcash/protos/src/main/proto/contact/v1/contact_list_service.proto index c78e4e273..5ed665c40 100644 --- a/definitions/flipcash/protos/src/main/proto/contact/v1/contact_list_service.proto +++ b/definitions/flipcash/protos/src/main/proto/contact/v1/contact_list_service.proto @@ -4,7 +4,6 @@ package flipcash.contact.v1; import "contact/v1/model.proto"; import "common/v1/common.proto"; -import "phone/v1/model.proto"; import "validate/validate.proto"; option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/contact/v1;contactpb"; @@ -64,9 +63,9 @@ message CheckSyncResponse { message DeltaUploadRequest { common.v1.Auth auth = 1 [(validate.rules).message.required = true]; - repeated phone.v1.PhoneNumber adds = 2 [(validate.rules).repeated.max_items = 1000]; + repeated common.v1.PhoneNumber adds = 2 [(validate.rules).repeated.max_items = 1000]; - repeated phone.v1.PhoneNumber removes = 3 [(validate.rules).repeated.max_items = 1000]; + repeated common.v1.PhoneNumber removes = 3 [(validate.rules).repeated.max_items = 1000]; // The checksum the client expected the server to have *before* applying // this delta. Server applies only if stored == old_checksum. @@ -97,7 +96,7 @@ message FullUploadRequest { // The complete current contact set. Server replaces stored state with // this list in one transaction. - repeated phone.v1.PhoneNumber phones = 2 [(validate.rules).repeated.max_items = 1000]; + repeated common.v1.PhoneNumber phones = 2 [(validate.rules).repeated.max_items = 1000]; // XOR-of-SHA256 over the client's current set of normalized E.164 phones. // Sent on the last streamed request to indicate the end of the upload. diff --git a/definitions/flipcash/protos/src/main/proto/contact/v1/model.proto b/definitions/flipcash/protos/src/main/proto/contact/v1/model.proto index 3d2853827..32d89ae59 100644 --- a/definitions/flipcash/protos/src/main/proto/contact/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/contact/v1/model.proto @@ -3,7 +3,6 @@ syntax = "proto3"; package flipcash.contact.v1; import "common/v1/common.proto"; -import "phone/v1/model.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -12,7 +11,7 @@ option java_package = "com.codeinc.flipcash.gen.contact.v1"; option objc_class_prefix = "FPBContactV1"; message FlipcashContact { - phone.v1.PhoneNumber phone = 1 [(validate.rules).message.required = true]; + common.v1.PhoneNumber phone = 1 [(validate.rules).message.required = true]; // The DM chat ID for the Flipcash contact. If the chat doesn't exist, it needs // to be initiated with a cash send to initialize it diff --git a/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto b/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto index 6a3870148..9e4662f6b 100644 --- a/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto +++ b/definitions/flipcash/protos/src/main/proto/email/v1/email_verification_service.proto @@ -26,7 +26,7 @@ service EmailVerification { message SendVerificationCodeRequest { // The email address to send a verification code to - EmailAddress email_address = 1 [(validate.rules).message.required = true]; + common.v1.EmailAddress email_address = 1 [(validate.rules).message.required = true]; common.v1.Auth auth = 2 [(validate.rules).message.required = true]; @@ -51,7 +51,7 @@ message SendVerificationCodeResponse { message CheckVerificationCodeRequest { // The email address being verified - EmailAddress email_address = 1 [(validate.rules).message.required = true]; + common.v1.EmailAddress email_address = 1 [(validate.rules).message.required = true]; // The verification code received via email VerificationCode code = 2 [(validate.rules).message.required = true]; @@ -81,7 +81,7 @@ message CheckVerificationCodeResponse { message UnlinkRequest { // The email address to unlink - EmailAddress email_address = 1 [(validate.rules).message.required = true]; + common.v1.EmailAddress email_address = 1 [(validate.rules).message.required = true]; common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } diff --git a/definitions/flipcash/protos/src/main/proto/email/v1/model.proto b/definitions/flipcash/protos/src/main/proto/email/v1/model.proto index 8efb34449..71aa2c305 100644 --- a/definitions/flipcash/protos/src/main/proto/email/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/email/v1/model.proto @@ -8,11 +8,6 @@ option objc_class_prefix = "FPBEmailV1"; import "validate/validate.proto"; -// EmailAddress is an email address -message EmailAddress { - string value = 1 [(validate.rules).string.pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"]; -} - // VerificationCode is a 4-10 digit numerical code for verification message VerificationCode { string value = 2 [(validate.rules).string = { diff --git a/definitions/flipcash/protos/src/main/proto/intent/v1/model.proto b/definitions/flipcash/protos/src/main/proto/intent/v1/model.proto index 430292e36..6cd69bcdf 100644 --- a/definitions/flipcash/protos/src/main/proto/intent/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/intent/v1/model.proto @@ -7,7 +7,6 @@ option java_package = "com.codeinc.flipcash.gen.intent.v1"; option objc_class_prefix = "FPBIntentV1"; import "common/v1/common.proto"; -import "phone/v1/model.proto"; import "validate/validate.proto"; message AppMetadata { @@ -32,10 +31,10 @@ message ChatMetadata { // For sending a payment to a contact in a DM message ContactDmPayment { // Source phone number that is paying. This is validated to be linked to the sender. - phone.v1.PhoneNumber source = 1 [(validate.rules).message.required = true]; + common.v1.PhoneNumber source = 1 [(validate.rules).message.required = true]; // Destination phone number that is being paid. This is validated to be linked to the receiver. - phone.v1.PhoneNumber destination = 2 [(validate.rules).message.required = true]; + common.v1.PhoneNumber destination = 2 [(validate.rules).message.required = true]; } // For sending a DM payment to someone using their user ID, which maps diff --git a/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto b/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto index 9a87c95f6..3c8247c2a 100644 --- a/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/phone/v1/model.proto @@ -8,12 +8,6 @@ option objc_class_prefix = "FPBPhoneV1"; import "validate/validate.proto"; -// PhoneNumber is an E.164 phone number -message PhoneNumber { - // Regex provided by Twilio here: https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 - string value = 1 [(validate.rules).string.pattern = "^\\+[1-9]\\d{1,14}$"]; -} - // VerificationCode is a 4-10 digit numerical code for verification message VerificationCode { string value = 2 [(validate.rules).string = { diff --git a/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto b/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto index 5229d6989..f5338660c 100644 --- a/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto +++ b/definitions/flipcash/protos/src/main/proto/phone/v1/phone_verification_service.proto @@ -29,7 +29,7 @@ service PhoneVerification { message SendVerificationCodeRequest { // The phone number to send a verification code over SMS to - PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; + common.v1.PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; // The app platform that's making this request common.v1.Platform platform = 2 [(validate.rules).enum = {in: [1,2]}]; @@ -55,7 +55,7 @@ message SendVerificationCodeResponse { message CheckVerificationCodeRequest { // The phone number being verified - PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; + common.v1.PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; // The verification code received via SMS VerificationCode code = 2 [(validate.rules).message.required = true]; @@ -85,7 +85,7 @@ message CheckVerificationCodeResponse { message UnlinkRequest { // The phone number to unlink - PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; + common.v1.PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } @@ -101,7 +101,7 @@ message UnlinkResponse { message LinkForPaymentRequest { // The phone number to link for payment - PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; + common.v1.PhoneNumber phone_number = 1 [(validate.rules).message.required = true]; common.v1.Auth auth = 2 [(validate.rules).message.required = true]; } diff --git a/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto b/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto index 47e89f9f3..20e447c27 100644 --- a/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/profile/v1/model.proto @@ -7,8 +7,7 @@ option java_package = "com.codeinc.flipcash.gen.profile.v1"; option objc_class_prefix = "FPBProfileV1"; import "blob/v1/model.proto"; -import "email/v1/model.proto"; -import "phone/v1/model.proto"; +import "common/v1/common.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -28,11 +27,11 @@ message UserProfile { // Phone number linked to this user. This is private and will only be returned // when the requesting user asks for their own profile - phone.v1.PhoneNumber phone_number = 3; + common.v1.PhoneNumber phone_number = 3; // Email address linked to this user. This is private and will only be returned // when the requesting user asks for their own profile - email.v1.EmailAddress email_address = 4; + common.v1.EmailAddress email_address = 4; // The user's profile picture, as the set of renditions it is stored as — // typically a DISPLAY for the profile view and a THUMBNAIL for avatars in @@ -47,6 +46,11 @@ message UserProfile { // Timestamp the user joined Flipcash google.protobuf.Timestamp join_ts = 6 [(validate.rules).timestamp.required = true]; + + // How the user has customized their Tip Card. Public, so it is returned for + // any user, not just the caller. Always set — the server resolves defaults + // for anything the user hasn't customized. Update it with UpdateTipCard. + TipCardCustomization tip_card_customization = 7 [(validate.rules).message.required = true]; } message SocialProfile { @@ -98,3 +102,10 @@ message XProfile { // The number of followers the user has on X uint32 follower_count = 7; } + +// Customization for a Tip Card +message TipCardCustomization { + // The colour of the Tip Card. Always set — the server falls back to the + // default colour when the user hasn't picked one. + common.v1.Color color = 1 [(validate.rules).message.required = true]; +} diff --git a/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto b/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto index f998b7488..e5e82b010 100644 --- a/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto +++ b/definitions/flipcash/protos/src/main/proto/profile/v1/profile_service.proto @@ -26,6 +26,10 @@ service Profile { // DISPLAY and THUMBNAIL renditions itself and returns the full set. rpc SetProfilePicture(SetProfilePictureRequest) returns (SetProfilePictureResponse); + // UpdateTipCard updates the caller's Tip Card customization. Every field is + // optional; only the ones set in the request are changed. + rpc UpdateTipCard(UpdateTipCardRequest) returns (UpdateTipCardResponse); + // LinkSocialAccount links a social account to a user rpc LinkSocialAccount(LinkSocialAccountRequest) returns (LinkSocialAccountResponse); @@ -104,6 +108,22 @@ message SetProfilePictureResponse { blob.v1.Media profile_picture = 2; } +message UpdateTipCardRequest { + // The new colour of the Tip Card. Left unchanged when unset. + common.v1.Color color = 1; + + common.v1.Auth auth = 10 [(validate.rules).message.required = true]; +} + +message UpdateTipCardResponse { + Result result = 1; + enum Result { + OK = 0; + DENIED = 1; + INVALID_COLOR = 2; + } +} + message LinkSocialAccountRequest { LinkingToken linking_token = 1 [(validate.rules).message.required = true]; diff --git a/definitions/flipcash/protos/src/main/proto/push/v1/model.proto b/definitions/flipcash/protos/src/main/proto/push/v1/model.proto index 35bfcd075..2ccc16e01 100644 --- a/definitions/flipcash/protos/src/main/proto/push/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/push/v1/model.proto @@ -8,7 +8,6 @@ option objc_class_prefix = "FPBPushV1"; import "chat/v1/model.proto"; import "common/v1/common.proto"; -import "phone/v1/model.proto"; import "validate/validate.proto"; enum TokenType { @@ -25,10 +24,10 @@ message Payload { Navigation navigation = 1; // Ordered substitutions to apply to push title - repeated Substitution title_substitutions = 2; + repeated common.v1.Substitution title_substitutions = 2; // Ordered substitutions to apply to push body - repeated Substitution body_substitutions = 3; + repeated common.v1.Substitution body_substitutions = 3; // Push notification category Category category = 4; @@ -62,25 +61,9 @@ message Navigation { common.v1.ChatId chat_id = 2; // Chat for a contact with the provided phone number - phone.v1.PhoneNumber chat_contact_phone_number = 3; + common.v1.PhoneNumber chat_contact_phone_number = 3; } } - -message Substitution { - // Fallback string for forwards compatibility - string fallback = 1 [(validate.rules).string = { - min_len: 1 - max_len: 4096 // Arbitrary - }]; - - oneof kind { - option (validate.required) = true; - - // Phone number -> contact name or formatted phone number - phone.v1.PhoneNumber contact = 2; - } -} - // Additional metadata provided for chat pushes message ChatMetadata { // The user ID that sent a chat message diff --git a/definitions/flipcash/protos/src/main/proto/resolver/v1/model.proto b/definitions/flipcash/protos/src/main/proto/resolver/v1/model.proto index 7a940c01f..48348406b 100644 --- a/definitions/flipcash/protos/src/main/proto/resolver/v1/model.proto +++ b/definitions/flipcash/protos/src/main/proto/resolver/v1/model.proto @@ -7,7 +7,6 @@ option java_package = "com.codeinc.flipcash.gen.resolver.v1"; option objc_class_prefix = "FPBResolverV1"; import "common/v1/common.proto"; -import "phone/v1/model.proto"; import "validate/validate.proto"; // Identifier wraps a real-world identifier that can be resolved to a @@ -16,8 +15,8 @@ message Identifier { oneof kind { option (validate.required) = true; - phone.v1.PhoneNumber phone = 1; - common.v1.UserId user_id = 2; + common.v1.PhoneNumber phone = 1; + common.v1.UserId user_id = 2; } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt index bebb7cbfa..fbb11dfe9 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt @@ -87,6 +87,16 @@ class ProfileController @Inject constructor( .onSuccess { mergeLocalProfile { it.copy(displayName = displayName) } } } + /** + * Updates the caller's tip card customization with the given hex color string. + */ + suspend fun updateTipCard(hexColor: String): Result { + val owner = userManager.accountCluster?.authority?.keyPair + ?: return Result.failure(Throwable("No account cluster in UserManager")) + + return repository.updateTipCard(owner, hexColor) + } + /** * Sets the caller's profile picture to a blob already uploaded via BlobStorage. * Returns the full set of renditions the server derived from it. diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ActivityFeedMessageMapper.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ActivityFeedMessageMapper.kt index ac343d258..728eaf6f3 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ActivityFeedMessageMapper.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ActivityFeedMessageMapper.kt @@ -6,6 +6,7 @@ import com.codeinc.flipcash.gen.common.v1.mintOrNull import com.flipcash.libs.currency.math.units import com.flipcash.services.internal.domain.mapper.Mapper import com.flipcash.services.internal.extensions.toPublicKey +import com.flipcash.services.internal.network.extensions.asSubstitution import com.flipcash.services.internal.network.extensions.toId import com.flipcash.services.internal.network.extensions.toMint import com.flipcash.services.internal.network.extensions.toPublicKey @@ -60,12 +61,28 @@ internal class ActivityFeedMessageMapper @Inject constructor( null -> NotificationState.UNKNOWN }, metadata = when (from.additionalMetadataCase) { - Model.Notification.AdditionalMetadataCase.DIRECTLY_SENT_CRYPTO -> NotificationMetadata.DirectlySentCrypto( - phoneNumber = from.directlySentCrypto.takeIf { it.hasPhone() }?.phone?.value - ) - Model.Notification.AdditionalMetadataCase.RECEIVED_CRYPTO -> NotificationMetadata.ReceivedCrypto( - phoneNumber = from.receivedCrypto.takeIf { it.hasPhone() }?.phone?.value - ) + Model.Notification.AdditionalMetadataCase.DIRECTLY_SENT_CRYPTO -> { + val meta = from.directlySentCrypto + NotificationMetadata.DirectlySentCrypto( + phoneNumber = if (meta.destinationIdentifierCase == + Model.DirectlySentCryptoNotificationMetadata.DestinationIdentifierCase.PHONE + ) meta.phone.value else null, + userId = if (meta.destinationIdentifierCase == + Model.DirectlySentCryptoNotificationMetadata.DestinationIdentifierCase.USER_ID + ) meta.userId.value.toByteArray().toList() else null, + ) + } + Model.Notification.AdditionalMetadataCase.RECEIVED_CRYPTO -> { + val meta = from.receivedCrypto + NotificationMetadata.ReceivedCrypto( + phoneNumber = if (meta.sourceIdentifierCase == + Model.ReceivedCryptoNotificationMetadata.SourceIdentifierCase.PHONE + ) meta.phone.value else null, + userId = if (meta.sourceIdentifierCase == + Model.ReceivedCryptoNotificationMetadata.SourceIdentifierCase.USER_ID + ) meta.userId.value.toByteArray().toList() else null, + ) + } Model.Notification.AdditionalMetadataCase.WITHDREW_CRYPTO -> NotificationMetadata.WithdrewCrypto Model.Notification.AdditionalMetadataCase.INDIRECTLY_SENT_CRYPTO -> NotificationMetadata.IndirectlySentCrypto( creator = from.indirectlySentCrypto.vault.value.toByteArray().toPublicKey(), @@ -76,7 +93,8 @@ internal class ActivityFeedMessageMapper @Inject constructor( Model.Notification.AdditionalMetadataCase.SOLD_CRYPTO -> NotificationMetadata.SoldToken Model.Notification.AdditionalMetadataCase.ADDITIONALMETADATA_NOT_SET, null -> NotificationMetadata.Unknown - } + }, + textSubstitutions = from.textSubstitutionsList.mapNotNull { it.asSubstitution() }, ) } } \ No newline at end of file diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ChatMetadataMapper.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ChatMetadataMapper.kt index f2d28ae8e..530805973 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ChatMetadataMapper.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ChatMetadataMapper.kt @@ -30,6 +30,7 @@ class ChatMetadataMapper @Inject constructor( lastActivity = Instant.fromEpochSeconds(from.lastActivity.seconds, from.lastActivity.nanos), latestEventSequence = from.latestEventSequence, isHidden = from.isHidden, + title = from.title.takeIf { it.isNotEmpty() }, ) } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt index 3a0ee614b..a9a660137 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt @@ -24,6 +24,7 @@ class UserProfileMapper @Inject constructor( joinedAt = if (from.hasJoinTs()) { Instant.fromEpochSeconds(from.joinTs.seconds, from.joinTs.nanos) } else null, + tipCardColor = if (from.hasTipCardCustomization()) from.tipCardCustomization.color.hex else null, ) } } \ No newline at end of file diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ContactListApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ContactListApi.kt index d6120e669..46dfb6272 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ContactListApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ContactListApi.kt @@ -1,7 +1,7 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.contact.v1.ContactListGrpcKt -import com.codeinc.flipcash.gen.phone.v1.Model +import com.codeinc.flipcash.gen.common.v1.Common import com.codeinc.flipcash.gen.contact.v1.validate import com.flipcash.services.models.ContactMethod import com.flipcash.services.internal.annotations.FlipcashManagedChannel @@ -52,8 +52,8 @@ internal class ContactListApi @Inject constructor( newChecksum: Checksum, ): RpcContactListService.DeltaUploadResponse { val request = RpcContactListService.DeltaUploadRequest.newBuilder() - .addAllAdds(adds.map { Model.PhoneNumber.newBuilder().setValue(it.phoneNumber).build() }) - .addAllRemoves(removes.map { Model.PhoneNumber.newBuilder().setValue(it.phoneNumber).build() }) + .addAllAdds(adds.map { Common.PhoneNumber.newBuilder().setValue(it.phoneNumber).build() }) + .addAllRemoves(removes.map { Common.PhoneNumber.newBuilder().setValue(it.phoneNumber).build() }) .setOldChecksum(oldChecksum.asHash()) .setNewChecksum(newChecksum.asHash()) .apply { setAuth(authenticate(owner)) } @@ -74,7 +74,7 @@ internal class ContactListApi @Inject constructor( val requestFlow = kotlinx.coroutines.flow.flow { phones.collect { batch -> val request = RpcContactListService.FullUploadRequest.newBuilder() - .addAllPhones(batch.map { Model.PhoneNumber.newBuilder().setValue(it.phoneNumber).build() }) + .addAllPhones(batch.map { Common.PhoneNumber.newBuilder().setValue(it.phoneNumber).build() }) .setExpectedChecksum(expectedChecksum.asHash()) .apply { setAuth(authenticate(owner)) } .build() diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt index 7cf6fce13..e8ca56740 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/EmailVerificationApi.kt @@ -3,6 +3,7 @@ package com.flipcash.services.internal.network.api import com.codeinc.flipcash.gen.email.v1.EmailVerificationGrpcKt import com.codeinc.flipcash.gen.email.v1.EmailVerificationService import com.codeinc.flipcash.gen.email.v1.Model +import com.codeinc.flipcash.gen.common.v1.Common import com.flipcash.services.internal.annotations.FlipcashManagedChannel import com.flipcash.services.internal.network.extensions.authenticate import com.flipcash.services.models.ContactMethod @@ -35,7 +36,7 @@ internal class EmailVerificationApi @Inject constructor( owner: Ed25519.KeyPair ): EmailVerificationService.SendVerificationCodeResponse { val request = EmailVerificationService.SendVerificationCodeRequest.newBuilder() - .setEmailAddress(Model.EmailAddress.newBuilder().setValue(request.emailAddress).build()) + .setEmailAddress(Common.EmailAddress.newBuilder().setValue(request.emailAddress).build()) .setClientData(request.clientData) .apply { setAuth(authenticate(owner)) } .build() @@ -56,7 +57,7 @@ internal class EmailVerificationApi @Inject constructor( owner: Ed25519.KeyPair ): EmailVerificationService.CheckVerificationCodeResponse { val request = EmailVerificationService.CheckVerificationCodeRequest.newBuilder() - .setEmailAddress(Model.EmailAddress.newBuilder().setValue(request.emailAddress).build()) + .setEmailAddress(Common.EmailAddress.newBuilder().setValue(request.emailAddress).build()) .setCode(Model.VerificationCode.newBuilder().setValue(code).build()) .apply { setAuth(authenticate(owner)) } .build() @@ -73,7 +74,7 @@ internal class EmailVerificationApi @Inject constructor( owner: Ed25519.KeyPair ): EmailVerificationService.UnlinkResponse { val request = EmailVerificationService.UnlinkRequest.newBuilder() - .setEmailAddress(Model.EmailAddress.newBuilder().setValue(request.emailAddress).build()) + .setEmailAddress(Common.EmailAddress.newBuilder().setValue(request.emailAddress).build()) .apply { setAuth(authenticate(owner)) } .build() diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt index 27bfd7642..0cf8fc42a 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PhoneVerificationApi.kt @@ -36,7 +36,7 @@ internal class PhoneVerificationApi @Inject constructor( owner: Ed25519.KeyPair ): PhoneVerificationService.SendVerificationCodeResponse { val request = PhoneVerificationService.SendVerificationCodeRequest.newBuilder() - .setPhoneNumber(Model.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) + .setPhoneNumber(Common.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) .setPlatform(Common.Platform.GOOGLE) .apply { setAuth(authenticate(owner)) } .build() @@ -57,7 +57,7 @@ internal class PhoneVerificationApi @Inject constructor( owner: Ed25519.KeyPair ): PhoneVerificationService.CheckVerificationCodeResponse { val request = PhoneVerificationService.CheckVerificationCodeRequest.newBuilder() - .setPhoneNumber(Model.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) + .setPhoneNumber(Common.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) .setCode(Model.VerificationCode.newBuilder().setValue(code).build()) .apply { setAuth(authenticate(owner)) } .build() @@ -77,7 +77,7 @@ internal class PhoneVerificationApi @Inject constructor( owner: Ed25519.KeyPair ): PhoneVerificationService.UnlinkResponse { val request = PhoneVerificationService.UnlinkRequest.newBuilder() - .setPhoneNumber(Model.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) + .setPhoneNumber(Common.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) .apply { setAuth(authenticate(owner)) } .build() @@ -93,7 +93,7 @@ internal class PhoneVerificationApi @Inject constructor( owner: Ed25519.KeyPair ): PhoneVerificationService.LinkForPaymentResponse { val request = PhoneVerificationService.LinkForPaymentRequest.newBuilder() - .setPhoneNumber(Model.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) + .setPhoneNumber(Common.PhoneNumber.newBuilder().setValue(request.phoneNumber).build()) .apply { setAuth(authenticate(owner)) } .build() diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt index 462d03192..016aff8cf 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt @@ -1,5 +1,6 @@ package com.flipcash.services.internal.network.api +import com.codeinc.flipcash.gen.common.v1.Common import com.codeinc.flipcash.gen.profile.v1.ProfileGrpcKt import com.codeinc.flipcash.gen.profile.v1.ProfileService import com.flipcash.services.internal.annotations.FlipcashManagedChannel @@ -104,6 +105,29 @@ internal class ProfileApi @Inject constructor( } } + /** + * Updates the caller's tip card customization with the given hex color. + */ + suspend fun updateTipCard( + owner: Ed25519.KeyPair, + hexColor: String, + ): ProfileService.UpdateTipCardResponse { + val request = ProfileService.UpdateTipCardRequest.newBuilder() + .setColor( + Common.Color.newBuilder() + .setHex(hexColor) + .build() + ) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.updateTipCard(request) + } + } + /** * removes a social account link from a user */ diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ResolverApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ResolverApi.kt index a2a0eb311..24544ca5e 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ResolverApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ResolverApi.kt @@ -1,6 +1,6 @@ package com.flipcash.services.internal.network.api -import com.codeinc.flipcash.gen.phone.v1.Model +import com.codeinc.flipcash.gen.common.v1.Common import com.codeinc.flipcash.gen.resolver.v1.ResolverGrpcKt import com.codeinc.flipcash.gen.resolver.v1.validate import com.flipcash.services.models.ResolveIdentifier @@ -47,7 +47,7 @@ internal class ResolverApi @Inject constructor( val builder = ResolverModel.Identifier.newBuilder() return when (this) { is ResolveIdentifier.Phone -> - builder.setPhone(Model.PhoneNumber.newBuilder().setValue(phone.phoneNumber)) + builder.setPhone(Common.PhoneNumber.newBuilder().setValue(phone.phoneNumber)) is ResolveIdentifier.UserId -> builder.setUserId(userId.asUserId()) }.build() diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt index 2d96ac15c..1837f0826 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt @@ -191,6 +191,7 @@ internal fun ChatType.asProtoChatType(): ChatModel.ChatType { ChatType.UNKNOWN -> ChatModel.ChatType.UNKNOWN ChatType.CONTACT_DM -> ChatModel.ChatType.CONTACT_DM ChatType.TIP_DM -> ChatModel.ChatType.TIP_DM + ChatType.GROUP -> ChatModel.ChatType.GROUP } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt index 8cc413ad2..b5cb3d75b 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt @@ -119,13 +119,18 @@ internal fun PushModels.Payload.asPayload(): NotificationPayload { ) } -internal fun PushModels.Substitution.asSubstitution(): Substitution? { +internal fun Common.Substitution.asSubstitution(): Substitution? { return when (kindCase) { - PushModels.Substitution.KindCase.CONTACT -> { - val phoneNumber = contact.value + Common.Substitution.KindCase.PHONE_NUMBER_TO_CONTACT_NAME -> { + val phoneNumber = phoneNumberToContactName.value Substitution.Phone(fallback = fallback, phoneNumber = phoneNumber) } + Common.Substitution.KindCase.USER_ID_TO_DISPLAY_NAME -> { + val userId = userIdToDisplayName.value.toByteArray().toList() + Substitution.UserId(fallback = fallback, userId = userId) + } + else -> null } } @@ -347,6 +352,7 @@ internal fun ChatModel.ChatType.toChatType(): ChatType { return when (this) { ChatModel.ChatType.CONTACT_DM -> ChatType.CONTACT_DM ChatModel.ChatType.TIP_DM -> ChatType.TIP_DM + ChatModel.ChatType.GROUP -> ChatType.GROUP else -> ChatType.UNKNOWN } } @@ -376,6 +382,7 @@ internal fun ChatModel.Metadata.toChatMetadata(): ChatMetadata { lastActivity = Instant.fromEpochSeconds(lastActivity.seconds, lastActivity.nanos), latestEventSequence = latestEventSequence, isHidden = isHidden, + title = title.takeIf { it.isNotEmpty() }, ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt index 63ff5f012..62346f0fa 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt @@ -12,6 +12,7 @@ import com.flipcash.services.models.SetProfilePictureError import com.flipcash.services.models.SocialAccountLinkRequest import com.flipcash.services.models.SocialAccountUnlinkRequest import com.flipcash.services.models.UnlinkSocialAccountError +import com.flipcash.services.models.UpdateTipCardError import com.flipcash.services.models.chat.BlobId import com.flipcash.services.models.chat.MediaItem import com.flipcash.services.internal.network.extensions.toMediaItem @@ -84,6 +85,26 @@ internal class ProfileService @Inject constructor( ) } + suspend fun updateTipCard( + owner: Ed25519.KeyPair, + hexColor: String, + ): Result { + return runCatching { + api.updateTipCard(owner, hexColor) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + ProfileService.UpdateTipCardResponse.Result.OK -> Result.success(Unit) + ProfileService.UpdateTipCardResponse.Result.DENIED -> Result.failure(UpdateTipCardError.Denied()) + ProfileService.UpdateTipCardResponse.Result.INVALID_COLOR -> Result.failure(UpdateTipCardError.InvalidColor()) + ProfileService.UpdateTipCardResponse.Result.UNRECOGNIZED -> Result.failure(UpdateTipCardError.Unrecognized()) + null -> Result.failure(UpdateTipCardError.Unrecognized()) + } + }, + onFailure = { Result.failure(it.toValidationOrElse { cause -> UpdateTipCardError.Other(cause) }) } + ) + } + suspend fun linkSocialAccount( request: SocialAccountLinkRequest, owner: Ed25519.KeyPair, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt index 3b9f90434..fb656310c 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt @@ -46,6 +46,14 @@ internal class InternalProfileRepository( .onFailure { ErrorUtils.handleError(it) } } + override suspend fun updateTipCard( + owner: Ed25519.KeyPair, + hexColor: String, + ): Result { + return service.updateTipCard(owner, hexColor) + .onFailure { ErrorUtils.handleError(it) } + } + override suspend fun linkSocialAccount( request: SocialAccountLinkRequest, owner: Ed25519.KeyPair diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/ActivityFeedNotification.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/ActivityFeedNotification.kt index 77d1b6a38..6a5af564b 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/ActivityFeedNotification.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/ActivityFeedNotification.kt @@ -24,7 +24,8 @@ data class ActivityFeedNotification( val amount: LocalFiat?, val timestamp: Instant, val state: NotificationState, - val metadata: NotificationMetadata? + val metadata: NotificationMetadata?, + val textSubstitutions: List = emptyList(), ) /** @@ -55,6 +56,7 @@ sealed interface NotificationMetadata { @Serializable data class DirectlySentCrypto( val phoneNumber: String? = null, + val userId: ID? = null, ) : NotificationMetadata /** @@ -73,6 +75,7 @@ sealed interface NotificationMetadata { @Serializable data class ReceivedCrypto( val phoneNumber: String? = null, + val userId: ID? = null, ) : NotificationMetadata @Serializable diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt index b78a9473a..72b221780 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt @@ -2,7 +2,6 @@ package com.flipcash.services.models import com.codeinc.flipcash.gen.common.v1.Common import com.codeinc.flipcash.gen.intent.v1.Model as FlipcashIntentModel -import com.codeinc.flipcash.gen.phone.v1.Model as PhoneModel import com.flipcash.services.models.chat.ChatId import com.getcode.utils.toByteString @@ -24,8 +23,8 @@ fun buildDmPaymentMetadata( .setChatId(Common.ChatId.newBuilder().setValue(chatId.bytes.toByteString())) .setContactDmPayment( FlipcashIntentModel.ChatMetadata.ContactDmPayment.newBuilder() - .setSource(PhoneModel.PhoneNumber.newBuilder().setValue(sourcePhone)) - .setDestination(PhoneModel.PhoneNumber.newBuilder().setValue(destinationPhone)) + .setSource(Common.PhoneNumber.newBuilder().setValue(sourcePhone)) + .setDestination(Common.PhoneNumber.newBuilder().setValue(destinationPhone)) ) ).build().toByteArray() } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt index a047d2edb..f157e1d0d 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt @@ -574,6 +574,16 @@ sealed class GetBlocklistError( data class Other(override val cause: Throwable? = null) : GetBlocklistError(message = cause?.message, cause = cause), NotifiableError } +sealed class UpdateTipCardError( + override val message: String? = null, + override val cause: Throwable? = null +): CodeServerError(message, cause) { + class Denied: UpdateTipCardError("Denied") + class InvalidColor: UpdateTipCardError("Invalid color") + class Unrecognized : UpdateTipCardError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : UpdateTipCardError(message = cause?.message, cause = cause), NotifiableError +} + // Thrown when a reserved blob failed server-side finalization (moderation / decode / size). // Terminal: the client must reserve a fresh upload to retry. class BlobRejectedException(val rejection: BlobRejection) : diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/NotificationPayload.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/NotificationPayload.kt index c5bb62af0..658bab11a 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/NotificationPayload.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/NotificationPayload.kt @@ -22,6 +22,11 @@ sealed interface Substitution { val fallback: String, val phoneNumber: String, ): Substitution + + data class UserId( + val fallback: String, + val userId: ID, + ): Substitution } /** diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt index 9f716c82c..844fabf84 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt @@ -18,6 +18,8 @@ data class UserProfile( val profilePicture: MediaItem? = null, // When the user joined Flipcash (server-provided). Null when unknown. val joinedAt: Instant? = null, + // The hex color for the user's tip card customization. Null when unset. + val tipCardColor: String? = null, ): Parcelable { /** The phone number only when it has been verified — backwards-compatible accessor. */ val verifiedPhoneNumber: String? get() = phoneNumber?.takeIf { it.verified }?.value @@ -31,6 +33,7 @@ data class UserProfile( socialAccounts = emptyList(), phoneNumber = null, email = null, + tipCardColor = null, ) } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatMetadata.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatMetadata.kt index 3ee4b9dfe..e19108551 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatMetadata.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatMetadata.kt @@ -10,4 +10,6 @@ data class ChatMetadata( val lastActivity: Instant, val latestEventSequence: Long = 0, val isHidden: Boolean = false, + // Title for this chat. Only set for group chats. + val title: String? = null, ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatType.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatType.kt index 08ab6763f..f4156d107 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatType.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatType.kt @@ -4,4 +4,5 @@ enum class ChatType { UNKNOWN, CONTACT_DM, TIP_DM, + GROUP, } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt index 4b968a326..113dbdb61 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt @@ -13,6 +13,7 @@ interface ProfileRepository { suspend fun getProfile(userId: ID, owner: Ed25519.KeyPair): Result suspend fun setDisplayName(displayName: String, owner: Ed25519.KeyPair): Result suspend fun setProfilePicture(blobId: BlobId, owner: Ed25519.KeyPair): Result + suspend fun updateTipCard(owner: Ed25519.KeyPair, hexColor: String): Result suspend fun linkSocialAccount(request: SocialAccountLinkRequest, owner: Ed25519.KeyPair): Result suspend fun unlinkSocialAccount(request: SocialAccountUnlinkRequest, owner: Ed25519.KeyPair): Result } \ No newline at end of file diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt index 116505209..1f0e1f1ef 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt @@ -302,12 +302,14 @@ private class FakeProfileRepository : ProfileRepository { var getProfileResult: Result = Result.failure(RuntimeException("not configured")) var setDisplayNameResult: Result = Result.success(Unit) var setProfilePictureResult: Result = Result.failure(RuntimeException("not configured")) + var updateTipCardResult: Result = Result.success(Unit) var linkSocialAccountResult: Result = Result.failure(RuntimeException("not configured")) var unlinkSocialAccountResult: Result = Result.success(Unit) override suspend fun getProfile(userId: ID, owner: Ed25519.KeyPair) = getProfileResult override suspend fun setDisplayName(displayName: String, owner: Ed25519.KeyPair) = setDisplayNameResult override suspend fun setProfilePicture(blobId: BlobId, owner: Ed25519.KeyPair) = setProfilePictureResult + override suspend fun updateTipCard(owner: Ed25519.KeyPair, hexColor: String) = updateTipCardResult override suspend fun linkSocialAccount(request: SocialAccountLinkRequest, owner: Ed25519.KeyPair) = linkSocialAccountResult override suspend fun unlinkSocialAccount(request: SocialAccountUnlinkRequest, owner: Ed25519.KeyPair) = diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt index 950253db8..3c41cd8e6 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt @@ -1,8 +1,8 @@ package com.flipcash.services.internal.domain import com.codeinc.flipcash.gen.blob.v1.Model as BlobModel -import com.codeinc.flipcash.gen.email.v1.emailAddress -import com.codeinc.flipcash.gen.phone.v1.phoneNumber +import com.codeinc.flipcash.gen.common.v1.emailAddress +import com.codeinc.flipcash.gen.common.v1.phoneNumber import com.codeinc.flipcash.gen.profile.v1.Model import com.codeinc.flipcash.gen.profile.v1.socialProfile import com.codeinc.flipcash.gen.profile.v1.xProfile diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/models/chat/DomainModelsTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/models/chat/DomainModelsTest.kt index 971d18f14..c910523a0 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/models/chat/DomainModelsTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/models/chat/DomainModelsTest.kt @@ -11,10 +11,11 @@ class DomainModelsTest { @Test fun `ChatType has expected values`() { - assertEquals(3, ChatType.entries.size) + assertEquals(4, ChatType.entries.size) assertIs(ChatType.UNKNOWN) assertIs(ChatType.CONTACT_DM) assertIs(ChatType.TIP_DM) + assertIs(ChatType.GROUP) } @Test From 453f1c66985bc4f6a69341dab1d17117202674c3 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 10 Aug 2026 19:36:06 -0400 Subject: [PATCH 2/4] feat(notifications): resolve userId push substitutions to display names The USER_ID_TO_DISPLAY_NAME substitution previously degraded to the server-provided fallback string. Resolve it properly via ProfileController.getProfileForUser (a network-backed lookup; cache-first off the normalized user_profiles table is a natural follow-up), falling back to the provided string only when it can't be resolved. --- .../app/notifications/NotificationService.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt index d1af116b2..ccc044a0c 100644 --- a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt +++ b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt @@ -28,6 +28,7 @@ import com.flipcash.app.contacts.ContactResolver import com.flipcash.app.core.util.Linkify import com.flipcash.shared.chat.ChatCoordinator import com.flipcash.app.tokens.TokenCoordinator +import com.flipcash.services.controllers.ProfileController import com.flipcash.services.controllers.PushController import com.flipcash.services.models.SocialAccount import com.flipcash.services.models.UserProfile @@ -93,6 +94,9 @@ class NotificationService : FirebaseMessagingService(), @Inject lateinit var chatCoordinator: ChatCoordinator + @Inject + lateinit var profileController: ProfileController + // TODO(firebase-messaging): 25.1.0 deprecated onNewToken in favor of FID-based onRegistered(). // Migrate once Firebase ships a stable guide and the backend accepts FID registration. // Tracking: https://github.com/firebase/firebase-android-sdk/issues/8087 @@ -344,9 +348,12 @@ class NotificationService : FirebaseMessagingService(), contactResolver.resolveName(substitution.phoneNumber, substitution.fallback) } is Substitution.UserId -> { - // Resolving a userId to a display name requires a network lookup not available here; - // degrade gracefully by using the server-provided fallback string. - substitution.fallback + // Resolve the user's display name via the profile lookup (network-backed today; + // cache-first off the normalized user_profiles table is a natural follow-up). + // Degrade to the server-provided fallback string if it can't be resolved. + profileController.getProfileForUser(substitution.userId).getOrNull() + ?.displayName?.takeIf { it.isNotBlank() } + ?: substitution.fallback } } } From 4564489076d4aa2bf804725d17fd75b8ac96292f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 10 Aug 2026 22:26:47 -0400 Subject: [PATCH 3/4] feat(notifications): resolve userId substitutions cache-first via user_profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the display name from the normalized user_profiles table first — fast and works offline, ideal for rendering a push — and only falls back to the network profile lookup when the user isn't cached (then to the server-provided fallback string). - UserProfileDao.getByUserId: cached-profile read by user id. - UserProfileDataSource.getCachedDisplayName: resolves the name, tolerating rows still carrying a not-yet-backfilled migration blob (via UserProfileEntity.toSerialized). --- .../app/notifications/NotificationService.kt | 22 ++++++++++----- .../app/persistence/dao/UserProfileDao.kt | 4 +++ .../sources/UserProfileDataSource.kt | 27 +++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/UserProfileDataSource.kt diff --git a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt index ccc044a0c..8c9c01e68 100644 --- a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt +++ b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt @@ -28,8 +28,10 @@ import com.flipcash.app.contacts.ContactResolver import com.flipcash.app.core.util.Linkify import com.flipcash.shared.chat.ChatCoordinator import com.flipcash.app.tokens.TokenCoordinator +import com.flipcash.app.persistence.sources.UserProfileDataSource import com.flipcash.services.controllers.ProfileController import com.flipcash.services.controllers.PushController +import com.getcode.opencode.model.core.ID import com.flipcash.services.models.SocialAccount import com.flipcash.services.models.UserProfile import com.flipcash.services.models.chat.ChatId @@ -97,6 +99,9 @@ class NotificationService : FirebaseMessagingService(), @Inject lateinit var profileController: ProfileController + @Inject + lateinit var userProfileDataSource: UserProfileDataSource + // TODO(firebase-messaging): 25.1.0 deprecated onNewToken in favor of FID-based onRegistered(). // Migrate once Firebase ships a stable guide and the backend accepts FID registration. // Tracking: https://github.com/firebase/firebase-android-sdk/issues/8087 @@ -348,16 +353,21 @@ class NotificationService : FirebaseMessagingService(), contactResolver.resolveName(substitution.phoneNumber, substitution.fallback) } is Substitution.UserId -> { - // Resolve the user's display name via the profile lookup (network-backed today; - // cache-first off the normalized user_profiles table is a natural follow-up). - // Degrade to the server-provided fallback string if it can't be resolved. - profileController.getProfileForUser(substitution.userId).getOrNull() - ?.displayName?.takeIf { it.isNotBlank() } - ?: substitution.fallback + resolveUserDisplayName(substitution.userId) ?: substitution.fallback } } } + /** + * Resolves [userId] to a display name, cache-first: the normalized `user_profiles` table is + * fast and works offline (ideal for rendering a push), and we only fall back to a network + * profile lookup when the user isn't cached. Returns null when neither resolves. + */ + private suspend fun resolveUserDisplayName(userId: ID): String? = + userProfileDataSource.getCachedDisplayName(userId) + ?: profileController.getProfileForUser(userId).getOrNull() + ?.displayName?.takeIf { it.isNotBlank() } + private suspend fun applySubstitutions(text: String, substitutions: List): String { var result = text for ((index, substitution) in substitutions.withIndex()) { diff --git a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt index 5a22e4582..2b4946a4e 100644 --- a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt +++ b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt @@ -50,6 +50,10 @@ interface UserProfileDao { ) suspend fun upsertNameAndAvatar(userIdHex: String, displayName: String, profilePicture: MediaItem?) + /** The cached profile for [userIdHex], or null if none is cached. */ + @Query("SELECT * FROM user_profiles WHERE user_id_hex = :userIdHex LIMIT 1") + suspend fun getByUserId(userIdHex: String): UserProfileEntity? + /** A batch of rows still carrying a staged legacy blob; drives [backfillMigratedProfiles]. */ @Query("SELECT * FROM user_profiles WHERE pending_migration_json IS NOT NULL LIMIT :limit") suspend fun pendingMigrationBatch(limit: Int): List diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/UserProfileDataSource.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/UserProfileDataSource.kt new file mode 100644 index 000000000..611b10b2b --- /dev/null +++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/UserProfileDataSource.kt @@ -0,0 +1,27 @@ +package com.flipcash.app.persistence.sources + +import com.flipcash.app.persistence.FlipcashDatabase +import com.flipcash.app.persistence.entities.toSerialized +import com.getcode.opencode.model.core.ID +import com.getcode.utils.hexEncodedString +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Read access to the normalized `user_profiles` cache, keyed by user id. Enables fast, + * offline-friendly profile lookups (e.g. resolving a push notification's userId substitution) + * without a network round-trip. + */ +@Singleton +class UserProfileDataSource @Inject constructor() { + + private val db: FlipcashDatabase? + get() = FlipcashDatabase.getInstance() + + /** The cached display name for [userId], or null if the user isn't cached (or has no name). */ + suspend fun getCachedDisplayName(userId: ID): String? { + val entity = db?.userProfileDao()?.getByUserId(userId.hexEncodedString()) ?: return null + // toSerialized() also resolves rows still carrying a not-yet-backfilled migration blob. + return entity.toSerialized().displayName?.takeIf { it.isNotBlank() } + } +} From e99a409944250c64756740d988059eec2cc1cf94 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 10 Aug 2026 23:21:27 -0400 Subject: [PATCH 4/4] refactor(chat): use a TODO stub for group chat id derivation Group chats use a server-assigned UUID rather than a derived DM id, and that path isn't wired up yet. A TODO() stub signals 'intentionally unimplemented' more clearly than error() (both still throw); UNKNOWN stays error() as a genuine invalid state. --- .../com/flipcash/shared/chat/internal/ChatIdGenerator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt index 240afcd54..152a812ad 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/ChatIdGenerator.kt @@ -25,7 +25,8 @@ class ChatIdGenerator @Inject constructor() { private fun ChatType.dmDomain(): String = when (this) { ChatType.CONTACT_DM -> DM_DOMAIN ChatType.TIP_DM -> TIP_DM_DOMAIN - ChatType.GROUP -> error("cannot derive a DM chat id for chat type $this") + // Group chats use a server-assigned UUID, not a derived DM id; wiring is not in yet. + ChatType.GROUP -> TODO("group chat id derivation is not implemented") ChatType.UNKNOWN -> error("cannot derive a DM chat id for chat type $this") }