Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ data class ActivityFeedMessage(
get() {
metadata ?: return false
val metadata =
(metadata as? MessageMetadata.SentCrypto) ?: return false
(metadata as? MessageMetadata.IndirectlySentCrypto) ?: return false
return metadata.canCancel
}
}
Expand All @@ -53,16 +53,20 @@ sealed interface MessageMetadata {
data object Unknown : MessageMetadata

@Serializable
data object GaveCrypto : MessageMetadata
data class DirectlySentCrypto(
val phoneNumber: String? = null,
) : MessageMetadata

@Serializable
data class SentCrypto(
data class IndirectlySentCrypto(
val creator: PublicKey,
val canCancel: Boolean,
) : MessageMetadata

@Serializable
data object ReceivedCrypto : MessageMetadata
data class ReceivedCrypto(
val phoneNumber: String? = null,
) : MessageMetadata

@Serializable
data object WithdrewCrypto : MessageMetadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,17 @@ class ActivityFeedMessageTest {
}

@Test
fun metadataFromGaveCrypto() {
val json = """{"type":"com.flipcash.app.core.feed.MessageMetadata.GaveCrypto"}"""
fun metadataFromDirectlySentCrypto() {
val json = """{"type":"com.flipcash.app.core.feed.MessageMetadata.DirectlySentCrypto"}"""
val result = MessageMetadata.from(json)
assertEquals(MessageMetadata.GaveCrypto, result)
assertEquals(MessageMetadata.DirectlySentCrypto(), result)
}

@Test
fun metadataFromReceivedCrypto() {
val json = """{"type":"com.flipcash.app.core.feed.MessageMetadata.ReceivedCrypto"}"""
val result = MessageMetadata.from(json)
assertEquals(MessageMetadata.ReceivedCrypto, result)
assertEquals(MessageMetadata.ReceivedCrypto(), result)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal fun ContentBubble(
isFromSelf = item.isFromSelf,
position = position,
)
is MessageContent.Cash -> Unit // TODO
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class TransactionHistoryViewModel @Inject constructor(
.filterIsInstance<Event.OnCancelRequested>()
.map { it.message }
.onEach { message ->
val metadata = message.metadata as? MessageMetadata.SentCrypto ?: return@onEach
val metadata = message.metadata as? MessageMetadata.IndirectlySentCrypto ?: return@onEach
val formattedAmount = message.amount?.formatted()
val title = formattedAmount?.let {
resources.getString(R.string.prompt_title_cancelTransferWithAmount, it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private val sampleItem = ActivityFeedMessage(
amount = oneDollarLocalized,
timestamp = Instant.parse("2025-06-03T16:25:00-04:00"),
state = MessageState.COMPLETED,
metadata = MessageMetadata.GaveCrypto
metadata = MessageMetadata.DirectlySentCrypto()
)

private val sampleItemWithToken = ActivityFeedMessageWithToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ sealed interface MessageContentSerialized {
@Serializable
@SerialName("text")
data class Text(val text: String) : MessageContentSerialized

@Serializable
@SerialName("cash")
data class Cash(val intentId: String, val quarks: Long) : MessageContentSerialized
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import com.flipcash.services.models.chat.MessagePointer
import com.flipcash.services.models.chat.PointerType
import com.flipcash.services.models.chat.ClientMessageId
import com.getcode.opencode.model.core.ID
import com.getcode.opencode.model.financial.Fiat
import com.getcode.utils.base58
import com.getcode.utils.base64
import com.getcode.utils.decodeBase58
import com.getcode.utils.decodeBase64
import com.getcode.utils.hexEncodedString
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -163,10 +168,18 @@ class ChatEntityMapper @Inject constructor() {

private fun MessageContent.toSerialized(): MessageContentSerialized = when (this) {
is MessageContent.Text -> MessageContentSerialized.Text(text)
is MessageContent.Cash -> MessageContentSerialized.Cash(
intentId = intentId.base58,
quarks = amount.quarks,
)
}

private fun MessageContentSerialized.toDomain(): MessageContent = when (this) {
is MessageContentSerialized.Text -> MessageContent.Text(text)
is MessageContentSerialized.Cash -> MessageContent.Cash(
intentId = intentId.decodeBase58().toList(),
amount = Fiat(quarks = quarks),
)
}

private fun MessagePointer.toSerialized(): MessagePointerSerialized = MessagePointerSerialized(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ class MetadataMapper @Inject constructor(): Mapper<NotificationMetadata?, Messag
override fun map(from: NotificationMetadata?): MessageMetadata? {
from ?: return null
return when (from) {
NotificationMetadata.GaveCrypto -> MessageMetadata.GaveCrypto
NotificationMetadata.ReceivedCrypto -> MessageMetadata.ReceivedCrypto
is NotificationMetadata.SentCrypto -> MessageMetadata.SentCrypto(from.creator, from.canCancel)
is NotificationMetadata.DirectlySentCrypto -> MessageMetadata.DirectlySentCrypto(from.phoneNumber)
is NotificationMetadata.ReceivedCrypto -> MessageMetadata.ReceivedCrypto(from.phoneNumber)
is NotificationMetadata.IndirectlySentCrypto -> MessageMetadata.IndirectlySentCrypto(from.creator, from.canCancel)
NotificationMetadata.Unknown -> MessageMetadata.Unknown
NotificationMetadata.WithdrewCrypto -> MessageMetadata.WithdrewCrypto
NotificationMetadata.DepositedCrypto -> MessageMetadata.DepositedCrypto
Expand Down
25 changes: 16 additions & 9 deletions definitions/flipcash/protos/src/main/proto/activity/v1/model.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 @@ -40,29 +41,35 @@ message Notification {

// Additional metadata for this notification specific to the notification
oneof additional_metadata {
GaveCryptoNotificationMetadata gave_crypto = 7;
ReceivedCryptoNotificationMetadata received_crypto = 8;
WithdrewCryptoNotificationMetadata withdrew_crypto = 9;
SentCryptoNotificationMetadata sent_crypto = 10;
DepositedCryptoNotificationMetadata deposited_crypto = 11;
BoughtCryptoNotificationMetadata bought_crypto = 12;
SoldCryptoNotificationMetadata sold_crypto = 13;
DirectlySentCryptoNotificationMetadata directly_sent_crypto = 7;
ReceivedCryptoNotificationMetadata received_crypto = 8;
WithdrewCryptoNotificationMetadata withdrew_crypto = 9;
IndirectlySentCryptoNotificationMetadata indirectly_sent_crypto = 10;
DepositedCryptoNotificationMetadata deposited_crypto = 11;
BoughtCryptoNotificationMetadata bought_crypto = 12;
SoldCryptoNotificationMetadata sold_crypto = 13;
}

reserved 6; // Deprecated WelcomeBonusNotificationMetadata
}

message GaveCryptoNotificationMetadata {
message DirectlySentCryptoNotificationMetadata {
oneof destination_identifier {
phone.v1.PhoneNumber phone = 1;
}
}

message ReceivedCryptoNotificationMetadata {
oneof source_identifier {
phone.v1.PhoneNumber phone = 1;
}
}

message WithdrewCryptoNotificationMetadata {
SwapState swap_state = 1 [(validate.rules).enum.not_in = 0];
}

message SentCryptoNotificationMetadata {
message IndirectlySentCryptoNotificationMetadata {
// The vault of the gift card account that was created for the cash link
common.v1.PublicKey vault = 1 [(validate.rules).message.required = true];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ message ChatId {
}];
}

message IntentId {
bytes value = 1 [(validate.rules).bytes = {
min_len: 32
max_len: 32
}];
}

// AppInstallId is a unque ID tied to a client app installation. It does not
// identify a device. Value should remain private and not be shared across
// installs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ syntax = "proto3";

package flipcash.contact.v1;

import "common/v1/common.proto";
import "phone/v1/model.proto";
import "validate/validate.proto";

Expand All @@ -11,4 +12,8 @@ option objc_class_prefix = "FPBContactV1";

message FlipcashContact {
phone.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
common.v1.ChatId dm_chat_id = 2 [(validate.rules).message.required = true];
}
39 changes: 39 additions & 0 deletions definitions/flipcash/protos/src/main/proto/intent/v1/model.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
syntax = "proto3";

package flipcash.intent.v1;

option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/intent/v1;intentpb";
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 {
oneof domain {
option (validate.required) = true;

ChatMetadata chat = 1;
}
}

// Additional metadata provided to SubmitIntent when doing payments in a chat
message ChatMetadata {
common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true];

oneof type {
option (validate.required) = true;

ContactDmPayment contact_dm_payment = 2;
}

// 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];

// 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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ message Content {
option (validate.required) = true;

TextContent text = 1;
CashContent cash = 2;
}
}

Expand All @@ -81,6 +82,17 @@ message TextContent {
}];
}

message CashContent {
// Intent ID identifying the cash transaction at the OCP layer
common.v1.IntentId intent_id = 1 [(validate.rules).message.required = true];

// The amount of cash that was sent
common.v1.CryptoPaymentAmount amount = 2 [(validate.rules).message.required = true];

// Reserved for receiver, which will is required for group chats
reserved 3;
}

// Pointer in a chat indicating a user's message history state in a chat.
message Pointer {
// The type of pointer indicates which user's message history state can be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ message Navigation {

// Currency info page for the provided mint
common.v1.PublicKey currency_info = 1;

// Chat for the provided ID
common.v1.ChatId chat_id = 2;
}
}

Expand All @@ -70,4 +73,4 @@ message Substitution {
// Phone number -> contact name or formatted phone number
phone.v1.PhoneNumber contact = 2;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ option java_package = "com.codeinc.opencode.gen.account.v1";
option objc_class_prefix = "CPBAccountV1";

import "common/v1/model.proto";
import "currency/v1/currency_service.proto";
import "transaction/v1/transaction_service.proto";
import "currency/v1/ocp_currency_service.proto";
import "transaction/v1/ocp_transaction_service.proto";
import "google/protobuf/timestamp.proto";
import "validate/validate.proto";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ option java_package = "com.codeinc.opencode.gen.messaging.v1";
option objc_class_prefix = "CPBMessagingV1";

import "common/v1/model.proto";
import "currency/v1/currency_service.proto";
import "transaction/v1/transaction_service.proto";
import "currency/v1/ocp_currency_service.proto";
import "transaction/v1/ocp_transaction_service.proto";
import "validate/validate.proto";

service Messaging {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ option java_package = "com.codeinc.opencode.gen.transaction.v1";
option objc_class_prefix = "APBTransactionV1";

import "common/v1/model.proto";
import "currency/v1/currency_service.proto";
import "currency/v1/ocp_currency_service.proto";
import "google/protobuf/timestamp.proto";
import "validate/validate.proto";

Expand Down Expand Up @@ -1020,6 +1020,9 @@ message Metadata {
ReceivePaymentsPubliclyMetadata receive_payments_publicly = 3;
PublicDistributionMetadata public_distribution = 4;
}

// Optional app-level metadata
AppMetadata app_metadata = 10;
}

// Open a set of accounts
Expand Down Expand Up @@ -1063,7 +1066,7 @@ message OpenAccountsMetadata {
// actions.push_back(FeePaymentAction(PRIMARY, feeAccount, feeAmount))
// }
//
// Action Spec (Remote Send):
// Action Spec (Indirect Send):
//
// actions = [
// OpenAccountAction(REMOTE_SEND_GIFT_CARD),
Expand Down Expand Up @@ -1101,7 +1104,7 @@ message SendPublicPaymentMetadata {
bool is_withdrawal = 5;

// Is the payment going to a new gift card? Note is_withdrawal must be false.
bool is_remote_send = 6;
bool is_indirect_send = 6;

// The mint that this intent will be operating against
common.v1.SolanaAccountId mint = 7 [(validate.rules).message.required = true];
Expand All @@ -1112,7 +1115,7 @@ message SendPublicPaymentMetadata {
// Receive funds into a user-owned account publicly. All use cases of this intent
// close the account, so all funds must be moved.
//
// Action Spec (Remote Send):
// Action Spec (Indirect Send):
//
// actions = [NoPrivacyWithdrawAction(REMOTE_SEND_GIFT_CARD, PRIMARY, quarks)]
message ReceivePaymentsPubliclyMetadata {
Expand All @@ -1128,11 +1131,11 @@ message ReceivePaymentsPubliclyMetadata {

// Is the receipt of funds from a remote send gift card? Currently, this is
// the only use case for this intent and validation enforces the flag to true.
bool is_remote_send = 3 [(validate.rules).bool.const = true];
bool is_indirect_send = 3 [(validate.rules).bool.const = true];



// If is_remote_send is true, the original exchange data that was provided as
// If is_indirect_send is true, the original exchange data that was provided as
// part of creating the gift card account. This is purely a server-provided value.
// SubmitIntent will disallow this being set.
ExchangeData exchange_data = 4;
Expand Down Expand Up @@ -1639,3 +1642,13 @@ enum FundingSource {
FUNDING_SOURCE_EXTERNAL_WALLET = 2;
FUNDING_SOURCE_COINBASE_ONRAMP = 3;
}

// AppMetadata is additional app-level metadata provided for an intent
message AppMetadata {
bytes value = 1 [(validate.rules).bytes = {
min_len: 1
max_len: 4096
}];


}
Loading
Loading