Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ sealed interface MessageMetadata {
@Serializable
data class DirectlySentCrypto(
val phoneNumber: String? = null,
val userId: ID? = null,
) : MessageMetadata

@Serializable
Expand All @@ -66,6 +67,7 @@ sealed interface MessageMetadata {
@Serializable
data class ReceivedCrypto(
val phoneNumber: String? = null,
val userId: ID? = null,
) : MessageMetadata

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class ChatIdGenerator @Inject constructor() {
private fun ChatType.dmDomain(): String = when (this) {
ChatType.CONTACT_DM -> DM_DOMAIN
ChatType.TIP_DM -> TIP_DM_DOMAIN
// 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")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +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
Expand Down Expand Up @@ -93,6 +96,12 @@ class NotificationService : FirebaseMessagingService(),
@Inject
lateinit var chatCoordinator: ChatCoordinator

@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
Expand Down Expand Up @@ -343,9 +352,22 @@ class NotificationService : FirebaseMessagingService(),
is Substitution.Phone -> {
contactResolver.resolveName(substitution.phoneNumber, substitution.fallback)
}
is Substitution.UserId -> {
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<Substitution>): String {
var result = text
for ((index, substitution) in substitutions.withIndex()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserProfileEntity>
Expand Down
Original file line number Diff line number Diff line change
@@ -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() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ class MetadataMapper @Inject constructor(): Mapper<NotificationMetadata?, Messag
override fun map(from: NotificationMetadata?): MessageMetadata? {
from ?: return null
return when (from) {
is NotificationMetadata.DirectlySentCrypto -> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum ChatType {
UNKNOWN = 0;
CONTACT_DM = 1;
TIP_DM = 2;
GROUP = 3;
}

message Metadata {
Expand All @@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}];
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -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];
Expand Down Expand Up @@ -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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading
Loading