diff --git a/CHANGELOG.md b/CHANGELOG.md index e601422..d9545bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,67 @@ called out explicitly even when nothing else did. release notes, so a version with no entry here does not release. Write the entry in the same PR that syncs the contract, while the diff is still in front of you. +## 0.23.0 + +Synced to [`flipcash2-protobuf-api@6de4f9ad`](https://github.com/code-payments/flipcash2-protobuf-api/commit/6de4f9adfd110a55b56fbfc99a5a466b3da789d1), +picking up [#160](https://github.com/code-payments/flipcash2-protobuf-api/pull/160), +[#161](https://github.com/code-payments/flipcash2-protobuf-api/pull/161), +[#162](https://github.com/code-payments/flipcash2-protobuf-api/pull/162) and +[#163](https://github.com/code-payments/flipcash2-protobuf-api/pull/163). `activity.v1`, +`chat.v1`, `event.v1`, `paymentrequest.v1` and `push.v1` moved. + +This release makes a public group's minimum balances editable, announces payment request changes +on the chat event stream, links feed payments to the payment request they paid, and adds a test +push. Upgrading breaks source in Swift only, not the wire. One enum gained cases, appended at the +end, so no existing value moved. + +### Breaking + +- `activity.v1.DirectlySentCryptoNotificationMetadata.boosted_reaction` and + `ReceivedCryptoNotificationMetadata.boosted_reaction` (field 3 in both) moved into a new + `oneof context`, next to `requested_payment`. Swift loses `hasBoostedReaction` and + `clearBoostedReaction` on both; switch on `context` instead. Kotlin keeps `hasBoostedReaction()` + and gains `getContextCase()`. +- `chat.v1.MetadataUpdate`'s `kind` oneof gains `rules_changed` (field 8). An exhaustive `switch` + over the Swift oneof, or a `when` over Kotlin's `KindCase` without an `else`, needs a case for + it before it compiles. + +### Added + +- `activity.v1.RequestedPayment`, with `chat_id` and `payment_request_id`, set as + `requested_payment` (oneof field 4) on both notification metadata messages above when the + payment paid a share of a payment request. It records what the payment was for and is not kept + up to date: the request may have closed, and fetching it may be denied. +- `chat.v1.EditChatRequest.rules` (field 6), the group's rules in full. Only a public group created + with a listener minimum balance can be edited, and only its listener and speaker + `MinimumBalanceRequirement` amounts may change; the proto docs list the limits. +- `EditChatResponse.Result` cases `INVALID_RULES = 7` and `RULES_NOT_SATISFIED = 8`, both + appended. +- `chat.v1.MetadataUpdate.RulesChanged`, carrying `new_rules` in full, sent on every rules edit. +- `event.v1.ChatUpdate.payment_request_updates` (field 10), a + `paymentrequest.v1.PaymentRequestUpdateBatch`. Like reaction updates, it is best effort and not + part of the gap-detected event log. +- `paymentrequest.v1.PaymentRequestUpdate`: a request whole when it is created (`created`), then + each change as `Updates` at the version it produced, made of `SharePaymentUpdate` and + `StatusUpdate`. The proto docs give the apply rules: ignore a version already held, apply the + next one, and refetch on a gap or on an update the client cannot apply. Expiry is never + announced. +- `push.v1.Push.SendTestPush`, with `SendTestPushRequest` and `SendTestPushResponse`. It sends a + fixed notification to every device the caller registered a token for. + +### Changed + +- A public group's `Metadata.rules` can now change after creation. A member who no longer + satisfies the new listener rules stays a member but cannot speak until they do. +- Payment request changes are announced in real time. Until now the docs told clients to refetch + a request to see payments and closes land; refetching is now only for reconciling a missed + update. + +### Unchanged + +Nothing was renumbered. No service, RPC, field or enum case was removed. Apart from the oneof +move above, no field changed type or number. + ## 0.22.0 Synced to [`flipcash2-protobuf-api@b655df17`](https://github.com/code-payments/flipcash2-protobuf-api/commit/b655df1770f1d588736ee3dcb712d9be5ca2cf41), diff --git a/Sources/Flipcash2ClientProtocol/ContractInfo.swift b/Sources/Flipcash2ClientProtocol/ContractInfo.swift index b05d09b..9b010dc 100644 --- a/Sources/Flipcash2ClientProtocol/ContractInfo.swift +++ b/Sources/Flipcash2ClientProtocol/ContractInfo.swift @@ -1,8 +1,8 @@ // Generated by scripts/generate-swift.sh. Do not edit. public enum Flipcash2ContractInfo { - public static let version = "0.22.0-dev" - public static let protoCommit = "b655df1770f1d588736ee3dcb712d9be5ca2cf41" + public static let version = "0.23.0-dev" + public static let protoCommit = "6de4f9adfd110a55b56fbfc99a5a466b3da789d1" public static var isLocal: Bool { protoCommit == localSentinel } diff --git a/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift index b629858..9a4e339 100644 --- a/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/activity_v1_model.pb.swift @@ -336,18 +336,33 @@ public struct Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: Senda set {destinationIdentifier = .userID(newValue)} } - /// Set when the payment boosted a reaction (see BoostedReaction). A boost - /// that paid one person names them in destination_identifier; one that - /// paid both the message's sender and the group's creator names neither, - /// and payment_amount is the whole boost. + /// What the payment was for, when it was made for something the feed can + /// link to. Unset for any other payment (a DM or group payment, one handed + /// off in person, ...). + public var context: Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata.OneOf_Context? = nil + + /// The payment boosted a reaction (see BoostedReaction). A boost that + /// paid one person names them in destination_identifier; one that paid + /// both the message's sender and the group's creator names neither, and + /// payment_amount is the whole boost. public var boostedReaction: Flipcash_Activity_V1_BoostedReaction { - get {return _boostedReaction ?? Flipcash_Activity_V1_BoostedReaction()} - set {_boostedReaction = newValue} + get { + if case .boostedReaction(let v)? = context {return v} + return Flipcash_Activity_V1_BoostedReaction() + } + set {context = .boostedReaction(newValue)} + } + + /// The payment paid the payer's share of a payment request (see + /// RequestedPayment). destination_identifier names the request's + /// creator, and payment_amount is the share. + public var requestedPayment: Flipcash_Activity_V1_RequestedPayment { + get { + if case .requestedPayment(let v)? = context {return v} + return Flipcash_Activity_V1_RequestedPayment() + } + set {context = .requestedPayment(newValue)} } - /// Returns true if `boostedReaction` has been explicitly set. - public var hasBoostedReaction: Bool {return self._boostedReaction != nil} - /// Clears the value of `boostedReaction`. Subsequent reads from it will return its default value. - public mutating func clearBoostedReaction() {self._boostedReaction = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -357,9 +372,23 @@ public struct Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: Senda } - public init() {} + /// What the payment was for, when it was made for something the feed can + /// link to. Unset for any other payment (a DM or group payment, one handed + /// off in person, ...). + public enum OneOf_Context: Equatable, Sendable { + /// The payment boosted a reaction (see BoostedReaction). A boost that + /// paid one person names them in destination_identifier; one that paid + /// both the message's sender and the group's creator names neither, and + /// payment_amount is the whole boost. + case boostedReaction(Flipcash_Activity_V1_BoostedReaction) + /// The payment paid the payer's share of a payment request (see + /// RequestedPayment). destination_identifier names the request's + /// creator, and payment_amount is the share. + case requestedPayment(Flipcash_Activity_V1_RequestedPayment) + + } - fileprivate var _boostedReaction: Flipcash_Activity_V1_BoostedReaction? = nil + public init() {} } public struct Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: Sendable { @@ -385,18 +414,33 @@ public struct Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: Sendable set {sourceIdentifier = .userID(newValue)} } - /// Set when the payment was the recipient's share of a boost of a - /// reaction (see BoostedReaction), paid to them as the boosted message's - /// sender or its group's creator. source_identifier names the payer, and + /// What the payment was for, when it was made for something the feed can + /// link to. Unset for any other payment (a DM or group payment, one handed + /// off in person, ...). + public var context: Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata.OneOf_Context? = nil + + /// The payment was the recipient's share of a boost of a reaction (see + /// BoostedReaction), paid to them as the boosted message's sender or its + /// group's creator. source_identifier names the payer, and /// payment_amount is the recipient's share alone. public var boostedReaction: Flipcash_Activity_V1_BoostedReaction { - get {return _boostedReaction ?? Flipcash_Activity_V1_BoostedReaction()} - set {_boostedReaction = newValue} + get { + if case .boostedReaction(let v)? = context {return v} + return Flipcash_Activity_V1_BoostedReaction() + } + set {context = .boostedReaction(newValue)} + } + + /// The payment was a debtor's share of the recipient's payment request + /// (see RequestedPayment). source_identifier names the debtor, and + /// payment_amount is the share. + public var requestedPayment: Flipcash_Activity_V1_RequestedPayment { + get { + if case .requestedPayment(let v)? = context {return v} + return Flipcash_Activity_V1_RequestedPayment() + } + set {context = .requestedPayment(newValue)} } - /// Returns true if `boostedReaction` has been explicitly set. - public var hasBoostedReaction: Bool {return self._boostedReaction != nil} - /// Clears the value of `boostedReaction`. Subsequent reads from it will return its default value. - public mutating func clearBoostedReaction() {self._boostedReaction = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -406,9 +450,23 @@ public struct Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: Sendable } - public init() {} + /// What the payment was for, when it was made for something the feed can + /// link to. Unset for any other payment (a DM or group payment, one handed + /// off in person, ...). + public enum OneOf_Context: Equatable, Sendable { + /// The payment was the recipient's share of a boost of a reaction (see + /// BoostedReaction), paid to them as the boosted message's sender or its + /// group's creator. source_identifier names the payer, and + /// payment_amount is the recipient's share alone. + case boostedReaction(Flipcash_Activity_V1_BoostedReaction) + /// The payment was a debtor's share of the recipient's payment request + /// (see RequestedPayment). source_identifier names the debtor, and + /// payment_amount is the share. + case requestedPayment(Flipcash_Activity_V1_RequestedPayment) + + } - fileprivate var _boostedReaction: Flipcash_Activity_V1_BoostedReaction? = nil + public init() {} } /// The reaction a payment boosted: the payer's reaction with emoji on a @@ -460,6 +518,44 @@ public struct Flipcash_Activity_V1_BoostedReaction: Sendable { fileprivate var _emoji: Flipcash_Messaging_V1_Emoji? = nil } +/// The payment request a payment paid a share of: a debtor's share, paid to +/// the request's creator (see intent.v1.ChatMetadata.RequestedPayment). It is +/// what the payment was for when it was made and is not kept up to date: the +/// request may since have been closed or expired, and the viewer may no longer +/// be a member of its group, in which case fetching the request is denied. +public struct Flipcash_Activity_V1_RequestedPayment: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The group chat the payment request is in. + public var chatID: Flipcash_Common_V1_ChatId { + get {return _chatID ?? Flipcash_Common_V1_ChatId()} + set {_chatID = newValue} + } + /// Returns true if `chatID` has been explicitly set. + public var hasChatID: Bool {return self._chatID != nil} + /// Clears the value of `chatID`. Subsequent reads from it will return its default value. + public mutating func clearChatID() {self._chatID = nil} + + /// The payment request whose share was paid. + public var paymentRequestID: Flipcash_Paymentrequest_V1_PaymentRequestId { + get {return _paymentRequestID ?? Flipcash_Paymentrequest_V1_PaymentRequestId()} + set {_paymentRequestID = newValue} + } + /// Returns true if `paymentRequestID` has been explicitly set. + public var hasPaymentRequestID: Bool {return self._paymentRequestID != nil} + /// Clears the value of `paymentRequestID`. Subsequent reads from it will return its default value. + public mutating func clearPaymentRequestID() {self._paymentRequestID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _chatID: Flipcash_Common_V1_ChatId? = nil + fileprivate var _paymentRequestID: Flipcash_Paymentrequest_V1_PaymentRequestId? = nil +} + public struct Flipcash_Activity_V1_WithdrewCryptoNotificationMetadata: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -916,7 +1012,7 @@ extension Flipcash_Activity_V1_Notification: SwiftProtobuf.Message, SwiftProtobu extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".DirectlySentCryptoNotificationMetadata" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0\u{3}boosted_reaction\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0\u{3}boosted_reaction\0\u{3}requested_payment\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -950,7 +1046,32 @@ extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProt self.destinationIdentifier = .userID(v) } }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._boostedReaction) }() + case 3: try { + var v: Flipcash_Activity_V1_BoostedReaction? + var hadOneofValue = false + if let current = self.context { + hadOneofValue = true + if case .boostedReaction(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.context = .boostedReaction(v) + } + }() + case 4: try { + var v: Flipcash_Activity_V1_RequestedPayment? + var hadOneofValue = false + if let current = self.context { + hadOneofValue = true + if case .requestedPayment(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.context = .requestedPayment(v) + } + }() default: break } } @@ -972,15 +1093,23 @@ extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProt }() case nil: break } - try { if let v = self._boostedReaction { + switch self.context { + case .boostedReaction?: try { + guard case .boostedReaction(let v)? = self.context else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() + }() + case .requestedPayment?: try { + guard case .requestedPayment(let v)? = self.context else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata, rhs: Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata) -> Bool { if lhs.destinationIdentifier != rhs.destinationIdentifier {return false} - if lhs._boostedReaction != rhs._boostedReaction {return false} + if lhs.context != rhs.context {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -988,7 +1117,7 @@ extension Flipcash_Activity_V1_DirectlySentCryptoNotificationMetadata: SwiftProt extension Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ReceivedCryptoNotificationMetadata" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0\u{3}boosted_reaction\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}phone\0\u{3}user_id\0\u{3}boosted_reaction\0\u{3}requested_payment\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -1022,7 +1151,32 @@ extension Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: SwiftProtobuf self.sourceIdentifier = .userID(v) } }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._boostedReaction) }() + case 3: try { + var v: Flipcash_Activity_V1_BoostedReaction? + var hadOneofValue = false + if let current = self.context { + hadOneofValue = true + if case .boostedReaction(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.context = .boostedReaction(v) + } + }() + case 4: try { + var v: Flipcash_Activity_V1_RequestedPayment? + var hadOneofValue = false + if let current = self.context { + hadOneofValue = true + if case .requestedPayment(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.context = .requestedPayment(v) + } + }() default: break } } @@ -1044,15 +1198,23 @@ extension Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata: SwiftProtobuf }() case nil: break } - try { if let v = self._boostedReaction { + switch self.context { + case .boostedReaction?: try { + guard case .boostedReaction(let v)? = self.context else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() + }() + case .requestedPayment?: try { + guard case .requestedPayment(let v)? = self.context else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata, rhs: Flipcash_Activity_V1_ReceivedCryptoNotificationMetadata) -> Bool { if lhs.sourceIdentifier != rhs.sourceIdentifier {return false} - if lhs._boostedReaction != rhs._boostedReaction {return false} + if lhs.context != rhs.context {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1102,6 +1264,45 @@ extension Flipcash_Activity_V1_BoostedReaction: SwiftProtobuf.Message, SwiftProt } } +extension Flipcash_Activity_V1_RequestedPayment: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".RequestedPayment" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{3}payment_request_id\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._chatID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._paymentRequestID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._chatID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._paymentRequestID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Activity_V1_RequestedPayment, rhs: Flipcash_Activity_V1_RequestedPayment) -> Bool { + if lhs._chatID != rhs._chatID {return false} + if lhs._paymentRequestID != rhs._paymentRequestID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Flipcash_Activity_V1_WithdrewCryptoNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".WithdrewCryptoNotificationMetadata" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}swap_state\0\u{3}swap_metadata\0") diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift index 9a56988..d892f55 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.grpc.swift @@ -850,11 +850,21 @@ extension Flipcash_Chat_V1_Chat { /// > already has is a no-op for that field, and a request that sets nothing /// > is a no-op that returns OK. /// > + /// > A public group's rules may also be edited, within limits (see + /// > EditChatRequest.rules); a private group has none to edit. New rules + /// > apply from the edit on to every check the rules decide, but never + /// > remove anyone: a member who no longer satisfies the listener rules + /// > stays a member and keeps reading, as when their balance falls, though + /// > like then they cannot speak until they satisfy them again, and once + /// > they leave they must satisfy them to rejoin. The server may take a + /// > short while to apply new rules everywhere. + /// > /// > Every real change reaches the chat's members, including the caller's /// > other devices, on the event stream as one MetadataUpdate per field /// > changed: TitleChanged for the title, ProfilePictureChanged for the /// > profile picture, DescriptionChanged for the description, - /// > CoverPictureChanged for the cover picture. + /// > CoverPictureChanged for the cover picture, RulesChanged for the + /// > rules. /// /// - Parameters: /// - request: A request containing a single `Flipcash_Chat_V1_EditChatRequest` message. @@ -2140,11 +2150,21 @@ extension Flipcash_Chat_V1_Chat { /// > already has is a no-op for that field, and a request that sets nothing /// > is a no-op that returns OK. /// > + /// > A public group's rules may also be edited, within limits (see + /// > EditChatRequest.rules); a private group has none to edit. New rules + /// > apply from the edit on to every check the rules decide, but never + /// > remove anyone: a member who no longer satisfies the listener rules + /// > stays a member and keeps reading, as when their balance falls, though + /// > like then they cannot speak until they satisfy them again, and once + /// > they leave they must satisfy them to rejoin. The server may take a + /// > short while to apply new rules everywhere. + /// > /// > Every real change reaches the chat's members, including the caller's /// > other devices, on the event stream as one MetadataUpdate per field /// > changed: TitleChanged for the title, ProfilePictureChanged for the /// > profile picture, DescriptionChanged for the description, - /// > CoverPictureChanged for the cover picture. + /// > CoverPictureChanged for the cover picture, RulesChanged for the + /// > rules. /// /// - Parameters: /// - request: A request containing a single `Flipcash_Chat_V1_EditChatRequest` message. @@ -3604,11 +3624,21 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { /// > already has is a no-op for that field, and a request that sets nothing /// > is a no-op that returns OK. /// > + /// > A public group's rules may also be edited, within limits (see + /// > EditChatRequest.rules); a private group has none to edit. New rules + /// > apply from the edit on to every check the rules decide, but never + /// > remove anyone: a member who no longer satisfies the listener rules + /// > stays a member and keeps reading, as when their balance falls, though + /// > like then they cannot speak until they satisfy them again, and once + /// > they leave they must satisfy them to rejoin. The server may take a + /// > short while to apply new rules everywhere. + /// > /// > Every real change reaches the chat's members, including the caller's /// > other devices, on the event stream as one MetadataUpdate per field /// > changed: TitleChanged for the title, ProfilePictureChanged for the /// > profile picture, DescriptionChanged for the description, - /// > CoverPictureChanged for the cover picture. + /// > CoverPictureChanged for the cover picture, RulesChanged for the + /// > rules. /// /// - Parameters: /// - request: A request containing a single `Flipcash_Chat_V1_EditChatRequest` message. @@ -4998,11 +5028,21 @@ extension Flipcash_Chat_V1_Chat.ClientProtocol { /// > already has is a no-op for that field, and a request that sets nothing /// > is a no-op that returns OK. /// > + /// > A public group's rules may also be edited, within limits (see + /// > EditChatRequest.rules); a private group has none to edit. New rules + /// > apply from the edit on to every check the rules decide, but never + /// > remove anyone: a member who no longer satisfies the listener rules + /// > stays a member and keeps reading, as when their balance falls, though + /// > like then they cannot speak until they satisfy them again, and once + /// > they leave they must satisfy them to rejoin. The server may take a + /// > short while to apply new rules everywhere. + /// > /// > Every real change reaches the chat's members, including the caller's /// > other devices, on the event stream as one MetadataUpdate per field /// > changed: TitleChanged for the title, ProfilePictureChanged for the /// > profile picture, DescriptionChanged for the description, - /// > CoverPictureChanged for the cover picture. + /// > CoverPictureChanged for the cover picture, RulesChanged for the + /// > rules. /// /// - Parameters: /// - message: request message to send. diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift index bee2f06..2618840 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_chat_service.pb.swift @@ -1299,6 +1299,37 @@ public struct Flipcash_Chat_V1_EditChatRequest: Sendable { /// Clears the value of `coverPicture`. Subsequent reads from it will return its default value. public mutating func clearCoverPicture() {self._coverPicture = nil} + /// The new rules, in full: every rule the chat is to carry, not only the + /// ones that change, as Metadata.rules shows them. Left unchanged when + /// unset. Only a public group created with a minimum listener balance + /// can have its rules edited; a private group is DENIED, and any other + /// group INVALID_RULES. + /// + /// The server currently accepts only the following changes from the + /// group's current rules, and refuses anything else as INVALID_RULES: + /// - The amount of the listener MinimumBalanceRequirement, which must + /// stay set, in the same currency and the same mints. + /// - Adding, removing, or changing the amount of a speaker + /// MinimumBalanceRequirement. If set, it must be in the listener + /// MinimumBalanceRequirement's currency and mints, and its amount must + /// be larger by at least the currency's minimum transfer value. + /// Every other rule (e.g. a StaffRequirement or a CreatorRequirement) + /// must be included exactly as the group carries it, in any order. Every + /// MinimumBalanceRequirement must be at least the currency's minimum + /// transfer value (one unit at its last decimal place, e.g. 0.01 USD or + /// 1 JPY), as at creation (see StartChatRequest.GroupChatParameters). + /// The caller must satisfy the new rules, speaker rules included + /// (RULES_NOT_SATISFIED otherwise). Rules equal to the group's current + /// ones are a no-op. + public var rules: Flipcash_Chat_V1_Rules { + get {return _rules ?? Flipcash_Chat_V1_Rules()} + set {_rules = newValue} + } + /// Returns true if `rules` has been explicitly set. + public var hasRules: Bool {return self._rules != nil} + /// Clears the value of `rules`. Subsequent reads from it will return its default value. + public mutating func clearRules() {self._rules = nil} + public var auth: Flipcash_Common_V1_Auth { get {return _auth ?? Flipcash_Common_V1_Auth()} set {_auth = newValue} @@ -1390,6 +1421,7 @@ public struct Flipcash_Chat_V1_EditChatRequest: Sendable { fileprivate var _profilePicture: Flipcash_Chat_V1_EditChatRequest.ProfilePicture? = nil fileprivate var _description_p: Flipcash_Chat_V1_EditChatRequest.Description? = nil fileprivate var _coverPicture: Flipcash_Chat_V1_EditChatRequest.CoverPicture? = nil + fileprivate var _rules: Flipcash_Chat_V1_Rules? = nil fileprivate var _auth: Flipcash_Common_V1_Auth? = nil } @@ -1428,6 +1460,8 @@ public struct Flipcash_Chat_V1_EditChatResponse: Sendable { case profilePictureBlobNotAccepted // = 4 case coverPictureBlobNotAccepted // = 5 case descriptionModerated // = 6 + case invalidRules // = 7 + case rulesNotSatisfied // = 8 case UNRECOGNIZED(Int) public init() { @@ -1443,6 +1477,8 @@ public struct Flipcash_Chat_V1_EditChatResponse: Sendable { case 4: self = .profilePictureBlobNotAccepted case 5: self = .coverPictureBlobNotAccepted case 6: self = .descriptionModerated + case 7: self = .invalidRules + case 8: self = .rulesNotSatisfied default: self = .UNRECOGNIZED(rawValue) } } @@ -1456,6 +1492,8 @@ public struct Flipcash_Chat_V1_EditChatResponse: Sendable { case .profilePictureBlobNotAccepted: return 4 case .coverPictureBlobNotAccepted: return 5 case .descriptionModerated: return 6 + case .invalidRules: return 7 + case .rulesNotSatisfied: return 8 case .UNRECOGNIZED(let i): return i } } @@ -1469,6 +1507,8 @@ public struct Flipcash_Chat_V1_EditChatResponse: Sendable { .profilePictureBlobNotAccepted, .coverPictureBlobNotAccepted, .descriptionModerated, + .invalidRules, + .rulesNotSatisfied, ] } @@ -4686,7 +4726,7 @@ extension Flipcash_Chat_V1_LeaveChatResponse.Result: SwiftProtobuf._ProtoNamePro extension Flipcash_Chat_V1_EditChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".EditChatRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}title\0\u{3}profile_picture\0\u{1}description\0\u{3}cover_picture\0\u{2}\u{5}auth\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}chat_id\0\u{1}title\0\u{3}profile_picture\0\u{1}description\0\u{3}cover_picture\0\u{1}rules\0\u{2}\u{4}auth\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4699,6 +4739,7 @@ extension Flipcash_Chat_V1_EditChatRequest: SwiftProtobuf.Message, SwiftProtobuf case 3: try { try decoder.decodeSingularMessageField(value: &self._profilePicture) }() case 4: try { try decoder.decodeSingularMessageField(value: &self._description_p) }() case 5: try { try decoder.decodeSingularMessageField(value: &self._coverPicture) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._rules) }() case 10: try { try decoder.decodeSingularMessageField(value: &self._auth) }() default: break } @@ -4725,6 +4766,9 @@ extension Flipcash_Chat_V1_EditChatRequest: SwiftProtobuf.Message, SwiftProtobuf try { if let v = self._coverPicture { try visitor.visitSingularMessageField(value: v, fieldNumber: 5) } }() + try { if let v = self._rules { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() try { if let v = self._auth { try visitor.visitSingularMessageField(value: v, fieldNumber: 10) } }() @@ -4737,6 +4781,7 @@ extension Flipcash_Chat_V1_EditChatRequest: SwiftProtobuf.Message, SwiftProtobuf if lhs._profilePicture != rhs._profilePicture {return false} if lhs._description_p != rhs._description_p {return false} if lhs._coverPicture != rhs._coverPicture {return false} + if lhs._rules != rhs._rules {return false} if lhs._auth != rhs._auth {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -4916,7 +4961,7 @@ extension Flipcash_Chat_V1_EditChatResponse: SwiftProtobuf.Message, SwiftProtobu } extension Flipcash_Chat_V1_EditChatResponse.Result: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}NOT_FOUND\0\u{1}TITLE_MODERATED\0\u{1}PROFILE_PICTURE_BLOB_NOT_ACCEPTED\0\u{1}COVER_PICTURE_BLOB_NOT_ACCEPTED\0\u{1}DESCRIPTION_MODERATED\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0\u{1}DENIED\0\u{1}NOT_FOUND\0\u{1}TITLE_MODERATED\0\u{1}PROFILE_PICTURE_BLOB_NOT_ACCEPTED\0\u{1}COVER_PICTURE_BLOB_NOT_ACCEPTED\0\u{1}DESCRIPTION_MODERATED\0\u{1}INVALID_RULES\0\u{1}RULES_NOT_SATISFIED\0") } extension Flipcash_Chat_V1_MuteChatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { diff --git a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift index 0300fef..413cb90 100644 --- a/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/chat_v1_model.pb.swift @@ -171,7 +171,8 @@ public struct Flipcash_Chat_V1_Metadata: @unchecked Sendable { public mutating func clearRosterSummary() {_uniqueStorage()._rosterSummary = nil} /// Rules governing participation in this chat. Only supported for group - /// chats. If not set, the chat has no participation requirements. Never + /// chats. If not set, the chat has no participation requirements. A + /// public group's minimum balances may change (see Chat.EditChat). Never /// set for a private group (see is_private), which admits a user by its /// creator's approval or an invite instead. public var rules: Flipcash_Chat_V1_Rules { @@ -813,6 +814,14 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { set {kind = .coverPictureChanged(newValue)} } + public var rulesChanged: Flipcash_Chat_V1_MetadataUpdate.RulesChanged { + get { + if case .rulesChanged(let v)? = kind {return v} + return Flipcash_Chat_V1_MetadataUpdate.RulesChanged() + } + set {kind = .rulesChanged(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Kind: Equatable, Sendable { @@ -823,6 +832,7 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { case profilePictureChanged(Flipcash_Chat_V1_MetadataUpdate.ProfilePictureChanged) case descriptionChanged(Flipcash_Chat_V1_MetadataUpdate.DescriptionChanged) case coverPictureChanged(Flipcash_Chat_V1_MetadataUpdate.CoverPictureChanged) + case rulesChanged(Flipcash_Chat_V1_MetadataUpdate.RulesChanged) } @@ -981,6 +991,32 @@ public struct Flipcash_Chat_V1_MetadataUpdate: Sendable { fileprivate var _newCoverPicture: Flipcash_Blob_V1_Media? = nil } + /// The chat's rules have changed (e.g. its minimum balances, via + /// Chat.EditChat). Delivered to the chat's members, including the editor's + /// other devices. Best-effort and applied as received; a client that + /// suspects a miss refetches the chat via Chat.GetChat. + public struct RulesChanged: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The chat's rules in full, replacing Metadata.rules. + public var newRules: Flipcash_Chat_V1_Rules { + get {return _newRules ?? Flipcash_Chat_V1_Rules()} + set {_newRules = newValue} + } + /// Returns true if `newRules` has been explicitly set. + public var hasNewRules: Bool {return self._newRules != nil} + /// Clears the value of `newRules`. Subsequent reads from it will return its default value. + public mutating func clearNewRules() {self._newRules = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _newRules: Flipcash_Chat_V1_Rules? = nil + } + public init() {} } @@ -3219,7 +3255,7 @@ extension Flipcash_Chat_V1_HoldingsSummary: SwiftProtobuf.Message, SwiftProtobuf extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".MetadataUpdate" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}full_refresh\0\u{3}last_activity_changed\0\u{3}viewer_state_changed\0\u{3}title_changed\0\u{3}profile_picture_changed\0\u{3}description_changed\0\u{3}cover_picture_changed\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}full_refresh\0\u{3}last_activity_changed\0\u{3}viewer_state_changed\0\u{3}title_changed\0\u{3}profile_picture_changed\0\u{3}description_changed\0\u{3}cover_picture_changed\0\u{3}rules_changed\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3318,6 +3354,19 @@ extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf. self.kind = .coverPictureChanged(v) } }() + case 8: try { + var v: Flipcash_Chat_V1_MetadataUpdate.RulesChanged? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .rulesChanged(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .rulesChanged(v) + } + }() default: break } } @@ -3357,6 +3406,10 @@ extension Flipcash_Chat_V1_MetadataUpdate: SwiftProtobuf.Message, SwiftProtobuf. guard case .coverPictureChanged(let v)? = self.kind else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 7) }() + case .rulesChanged?: try { + guard case .rulesChanged(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -3599,6 +3652,40 @@ extension Flipcash_Chat_V1_MetadataUpdate.CoverPictureChanged: SwiftProtobuf.Mes } } +extension Flipcash_Chat_V1_MetadataUpdate.RulesChanged: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Chat_V1_MetadataUpdate.protoMessageName + ".RulesChanged" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}new_rules\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._newRules) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._newRules { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Chat_V1_MetadataUpdate.RulesChanged, rhs: Flipcash_Chat_V1_MetadataUpdate.RulesChanged) -> Bool { + if lhs._newRules != rhs._newRules {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Flipcash_Chat_V1_RosterUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".RosterUpdate" public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}member_joined\0\u{3}member_left\0\u{3}membership_changed\0\u{4}\u{7}roster_summary\0") diff --git a/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift index cc08ace..5318592 100644 --- a/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/event_v1_model.pb.swift @@ -371,6 +371,23 @@ public struct Flipcash_Event_V1_ChatUpdate: Sendable { /// Clears the value of `lobbyUpdates`. Subsequent reads from it will return its default value. public mutating func clearLobbyUpdates() {self._lobbyUpdates = nil} + /// If present, best-effort real-time changes to the chat's payment + /// requests: one created, carried whole, and each change after that (a + /// share paid, a request completed or closed), carried as what it did. + /// Sent to those who can read the chat in full, never to a redacted chat + /// preview. Like reaction_updates, they are NOT part of the gap-detected + /// event log; clients apply each to their copy by the request's version + /// and reconcile any misses by refetching the request (see + /// paymentrequest.v1.PaymentRequestUpdate). + public var paymentRequestUpdates: Flipcash_Paymentrequest_V1_PaymentRequestUpdateBatch { + get {return _paymentRequestUpdates ?? Flipcash_Paymentrequest_V1_PaymentRequestUpdateBatch()} + set {_paymentRequestUpdates = newValue} + } + /// Returns true if `paymentRequestUpdates` has been explicitly set. + public var hasPaymentRequestUpdates: Bool {return self._paymentRequestUpdates != nil} + /// Clears the value of `paymentRequestUpdates`. Subsequent reads from it will return its default value. + public mutating func clearPaymentRequestUpdates() {self._paymentRequestUpdates = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -382,6 +399,7 @@ public struct Flipcash_Event_V1_ChatUpdate: Sendable { fileprivate var _reactionUpdates: Flipcash_Messaging_V1_ReactionUpdateBatch? = nil fileprivate var _rosterUpdates: Flipcash_Chat_V1_RosterUpdateBatch? = nil fileprivate var _lobbyUpdates: Flipcash_Chat_V1_LobbyUpdateBatch? = nil + fileprivate var _paymentRequestUpdates: Flipcash_Paymentrequest_V1_PaymentRequestUpdateBatch? = nil } /// BlobUpdate notifies the recipient in real time that blobs they uploaded have @@ -827,7 +845,7 @@ extension Flipcash_Event_V1_ClientPong: SwiftProtobuf.Message, SwiftProtobuf._Me extension Flipcash_Event_V1_ChatUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ChatUpdate" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}chat\0\u{4}\u{2}pointer_updates\0\u{3}is_typing_notifications\0\u{3}metadata_updates\0\u{1}events\0\u{3}reaction_updates\0\u{3}roster_updates\0\u{3}lobby_updates\0\u{c}\u{2}\u{1}") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}chat\0\u{4}\u{2}pointer_updates\0\u{3}is_typing_notifications\0\u{3}metadata_updates\0\u{1}events\0\u{3}reaction_updates\0\u{3}roster_updates\0\u{3}lobby_updates\0\u{3}payment_request_updates\0\u{c}\u{2}\u{1}") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -843,6 +861,7 @@ extension Flipcash_Event_V1_ChatUpdate: SwiftProtobuf.Message, SwiftProtobuf._Me case 7: try { try decoder.decodeSingularMessageField(value: &self._reactionUpdates) }() case 8: try { try decoder.decodeSingularMessageField(value: &self._rosterUpdates) }() case 9: try { try decoder.decodeSingularMessageField(value: &self._lobbyUpdates) }() + case 10: try { try decoder.decodeSingularMessageField(value: &self._paymentRequestUpdates) }() default: break } } @@ -877,6 +896,9 @@ extension Flipcash_Event_V1_ChatUpdate: SwiftProtobuf.Message, SwiftProtobuf._Me try { if let v = self._lobbyUpdates { try visitor.visitSingularMessageField(value: v, fieldNumber: 9) } }() + try { if let v = self._paymentRequestUpdates { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -889,6 +911,7 @@ extension Flipcash_Event_V1_ChatUpdate: SwiftProtobuf.Message, SwiftProtobuf._Me if lhs._reactionUpdates != rhs._reactionUpdates {return false} if lhs._rosterUpdates != rhs._rosterUpdates {return false} if lhs._lobbyUpdates != rhs._lobbyUpdates {return false} + if lhs._paymentRequestUpdates != rhs._paymentRequestUpdates {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Flipcash2ClientProtocol/paymentrequest_v1_model.pb.swift b/Sources/Flipcash2ClientProtocol/paymentrequest_v1_model.pb.swift index c29be70..1241fd1 100644 --- a/Sources/Flipcash2ClientProtocol/paymentrequest_v1_model.pb.swift +++ b/Sources/Flipcash2ClientProtocol/paymentrequest_v1_model.pb.swift @@ -132,9 +132,11 @@ public struct Flipcash_Paymentrequest_V1_IdempotencyKey: Sendable { /// EXPIRED over OPEN, and judges an OPEN request past its ends_at as EXPIRED /// itself. /// -/// Nothing announces a change in real time: no event carries a payment request -/// today. Clients refetch a request to see payments and closes land, for -/// example when they show its widget. +/// Changes are announced in real time, best effort, by a PaymentRequestUpdate +/// on the chat's event stream (event.v1.ChatUpdate.payment_request_updates): +/// the request whole when it is created, and each change after that as the +/// changes it made at its version. Clients refetch a request to reconcile any +/// update they missed, for example when they show its widget. /// /// Requests are layered. The first layer is where the request lives: today a /// group chat (GroupChatPaymentRequest). Within it, the second layer is the @@ -617,6 +619,213 @@ public struct Flipcash_Paymentrequest_V1_PaidShare: Sendable { fileprivate var _paidAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil } +/// PaymentRequestUpdate is a best-effort, real-time change to a payment +/// request, carried on its chat's event stream (see +/// event.v1.ChatUpdate.payment_request_updates). It is sent when a request is +/// created, carrying it whole (`created`), and on every change that advances +/// its version, carrying what that change did (`updates`): a share paid, by +/// payment or by hand, or a hand mark replaced by a payment; the last share +/// paid, which also completes the request; and a close. Expiry is not +/// announced: it comes from the clock and moves no version, so clients judge +/// it from ends_at (see PAYMENT_REQUEST_STATUS_EXPIRED). +/// +/// Updates may arrive out of order, more than once, or not at all. A client +/// applies them to its copy of the request by version: +/// - `created`: like a fetched copy, keeping the one with the greater +/// version (at an equal version, EXPIRED over OPEN). +/// - `updates` at version V, to a copy at version L: +/// - V <= L: already applied; ignore it. +/// - V == L + 1: apply every Update, in order, then set version to V. +/// - V > L + 1: an update was missed; refetch the request. +/// A client holding no copy ignores it and fetches the request when it +/// shows it. A client that meets an Update it cannot apply (a kind it does +/// not know, or a share its copy does not have) applies none of them and +/// refetches, rather than taking version V for a copy missing a change. +/// +/// A missed update is reconciled by refetching the request. A share paid by a +/// payment is also announced by the payment's messaging.v1.CashContent +/// message, which carries the request's payment_request_id and is part of the +/// chat's gap-detected event log, so a client that sees one for a share it +/// holds unpaid can refetch the request. +/// +/// Like GetPaymentRequest, an update reaches only those who can read the chat +/// in full. It never carries the caller-specific payment_intent_id of +/// GetPaymentRequestResponse; a debtor fetches the request to pay their share. +public struct Flipcash_Paymentrequest_V1_PaymentRequestUpdate: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var paymentRequestID: Flipcash_Paymentrequest_V1_PaymentRequestId { + get {return _storage._paymentRequestID ?? Flipcash_Paymentrequest_V1_PaymentRequestId()} + set {_uniqueStorage()._paymentRequestID = newValue} + } + /// Returns true if `paymentRequestID` has been explicitly set. + public var hasPaymentRequestID: Bool {return _storage._paymentRequestID != nil} + /// Clears the value of `paymentRequestID`. Subsequent reads from it will return its default value. + public mutating func clearPaymentRequestID() {_uniqueStorage()._paymentRequestID = nil} + + public var kind: OneOf_Kind? { + get {return _storage._kind} + set {_uniqueStorage()._kind = newValue} + } + + /// The request was created. It is carried whole, as GetPaymentRequest + /// returns it, and is applied like a fetched copy. + public var created: Flipcash_Paymentrequest_V1_PaymentRequestMetadata { + get { + if case .created(let v)? = _storage._kind {return v} + return Flipcash_Paymentrequest_V1_PaymentRequestMetadata() + } + set {_uniqueStorage()._kind = .created(newValue)} + } + + /// One change to the request: everything it did, at one version. + public var updates: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Updates { + get { + if case .updates(let v)? = _storage._kind {return v} + return Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Updates() + } + set {_uniqueStorage()._kind = .updates(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_Kind: Equatable, Sendable { + /// The request was created. It is carried whole, as GetPaymentRequest + /// returns it, and is applied like a fetched copy. + case created(Flipcash_Paymentrequest_V1_PaymentRequestMetadata) + /// One change to the request: everything it did, at one version. + case updates(Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Updates) + + } + + /// Updates is everything one change to the request did, at the version + /// it produced. + public struct Updates: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The request's version after these updates. It advances by exactly + /// one per change, so a client holding version - 1 applies them, and + /// one holding less has missed one. + public var version: UInt64 = 0 + + /// What the change did, applied in order, all or nothing. A + /// StatusUpdate is at most one, and the last. + public var updates: [Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Update] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + + public struct Update: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var kind: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Update.OneOf_Kind? = nil + + public var sharePaymentUpdate: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.SharePaymentUpdate { + get { + if case .sharePaymentUpdate(let v)? = kind {return v} + return Flipcash_Paymentrequest_V1_PaymentRequestUpdate.SharePaymentUpdate() + } + set {kind = .sharePaymentUpdate(newValue)} + } + + public var statusUpdate: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.StatusUpdate { + get { + if case .statusUpdate(let v)? = kind {return v} + return Flipcash_Paymentrequest_V1_PaymentRequestUpdate.StatusUpdate() + } + set {kind = .statusUpdate(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_Kind: Equatable, Sendable { + case sharePaymentUpdate(Flipcash_Paymentrequest_V1_PaymentRequestUpdate.SharePaymentUpdate) + case statusUpdate(Flipcash_Paymentrequest_V1_PaymentRequestUpdate.StatusUpdate) + + } + + public init() {} + } + + /// SharePaymentUpdate is a share paid, by payment or by hand, or a hand + /// mark replaced by a payment (see PaidShare). Clients replace the share + /// with the same user_id by this one. + public struct SharePaymentUpdate: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The share as it now stands. Its paid is always set. + public var share: Flipcash_Paymentrequest_V1_Share { + get {return _share ?? Flipcash_Paymentrequest_V1_Share()} + set {_share = newValue} + } + /// Returns true if `share` has been explicitly set. + public var hasShare: Bool {return self._share != nil} + /// Clears the value of `share`. Subsequent reads from it will return its default value. + public mutating func clearShare() {self._share = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _share: Flipcash_Paymentrequest_V1_Share? = nil + } + + /// StatusUpdate is the request leaving OPEN: CLOSED by its creator, or + /// COMPLETED by the payment of its last unpaid share, which comes in the + /// same Updates, before it. Both are final, so a request's status changes + /// at most once. Expiry is never announced (see above). + public struct StatusUpdate: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var status: Flipcash_Paymentrequest_V1_PaymentRequestStatus = .unknown + + /// When the request stopped being OPEN (see + /// PaymentRequestMetadata.closed_at). + public var closedAt: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _closedAt ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_closedAt = newValue} + } + /// Returns true if `closedAt` has been explicitly set. + public var hasClosedAt: Bool {return self._closedAt != nil} + /// Clears the value of `closedAt`. Subsequent reads from it will return its default value. + public mutating func clearClosedAt() {self._closedAt = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _closedAt: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + } + + public init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +public struct Flipcash_Paymentrequest_V1_PaymentRequestUpdateBatch: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var paymentRequestUpdates: [Flipcash_Paymentrequest_V1_PaymentRequestUpdate] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "flipcash.paymentrequest.v1" @@ -1143,3 +1352,318 @@ extension Flipcash_Paymentrequest_V1_PaidShare.Creator: SwiftProtobuf.Message, S return true } } + +extension Flipcash_Paymentrequest_V1_PaymentRequestUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".PaymentRequestUpdate" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}payment_request_id\0\u{2}\u{9}created\0\u{1}updates\0") + + fileprivate class _StorageClass { + var _paymentRequestID: Flipcash_Paymentrequest_V1_PaymentRequestId? = nil + var _kind: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.OneOf_Kind? + + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _paymentRequestID = source._paymentRequestID + _kind = source._kind + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + public mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._paymentRequestID) }() + case 10: try { + var v: Flipcash_Paymentrequest_V1_PaymentRequestMetadata? + var hadOneofValue = false + if let current = _storage._kind { + hadOneofValue = true + if case .created(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._kind = .created(v) + } + }() + case 11: try { + var v: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Updates? + var hadOneofValue = false + if let current = _storage._kind { + hadOneofValue = true + if case .updates(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._kind = .updates(v) + } + }() + default: break + } + } + } + } + + public func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._paymentRequestID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + switch _storage._kind { + case .created?: try { + guard case .created(let v)? = _storage._kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() + case .updates?: try { + guard case .updates(let v)? = _storage._kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case nil: break + } + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate, rhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._paymentRequestID != rhs_storage._paymentRequestID {return false} + if _storage._kind != rhs_storage._kind {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Updates: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Paymentrequest_V1_PaymentRequestUpdate.protoMessageName + ".Updates" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{1}updates\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.version) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.updates) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.version != 0 { + try visitor.visitSingularUInt64Field(value: self.version, fieldNumber: 1) + } + if !self.updates.isEmpty { + try visitor.visitRepeatedMessageField(value: self.updates, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Updates, rhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Updates) -> Bool { + if lhs.version != rhs.version {return false} + if lhs.updates != rhs.updates {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Update: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Paymentrequest_V1_PaymentRequestUpdate.protoMessageName + ".Update" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}share_payment_update\0\u{3}status_update\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.SharePaymentUpdate? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .sharePaymentUpdate(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .sharePaymentUpdate(v) + } + }() + case 2: try { + var v: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.StatusUpdate? + var hadOneofValue = false + if let current = self.kind { + hadOneofValue = true + if case .statusUpdate(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.kind = .statusUpdate(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.kind { + case .sharePaymentUpdate?: try { + guard case .sharePaymentUpdate(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .statusUpdate?: try { + guard case .statusUpdate(let v)? = self.kind else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Update, rhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.Update) -> Bool { + if lhs.kind != rhs.kind {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Paymentrequest_V1_PaymentRequestUpdate.SharePaymentUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Paymentrequest_V1_PaymentRequestUpdate.protoMessageName + ".SharePaymentUpdate" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}share\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._share) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._share { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.SharePaymentUpdate, rhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.SharePaymentUpdate) -> Bool { + if lhs._share != rhs._share {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Paymentrequest_V1_PaymentRequestUpdate.StatusUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Flipcash_Paymentrequest_V1_PaymentRequestUpdate.protoMessageName + ".StatusUpdate" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}status\0\u{3}closed_at\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.status) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._closedAt) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.status != .unknown { + try visitor.visitSingularEnumField(value: self.status, fieldNumber: 1) + } + try { if let v = self._closedAt { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.StatusUpdate, rhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdate.StatusUpdate) -> Bool { + if lhs.status != rhs.status {return false} + if lhs._closedAt != rhs._closedAt {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Paymentrequest_V1_PaymentRequestUpdateBatch: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".PaymentRequestUpdateBatch" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}payment_request_updates\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.paymentRequestUpdates) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.paymentRequestUpdates.isEmpty { + try visitor.visitRepeatedMessageField(value: self.paymentRequestUpdates, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdateBatch, rhs: Flipcash_Paymentrequest_V1_PaymentRequestUpdateBatch) -> Bool { + if lhs.paymentRequestUpdates != rhs.paymentRequestUpdates {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/Flipcash2ClientProtocol/paymentrequest_v1_paymentrequest_service.grpc.swift b/Sources/Flipcash2ClientProtocol/paymentrequest_v1_paymentrequest_service.grpc.swift index 713f415..f92fb2b 100644 --- a/Sources/Flipcash2ClientProtocol/paymentrequest_v1_paymentrequest_service.grpc.swift +++ b/Sources/Flipcash2ClientProtocol/paymentrequest_v1_paymentrequest_service.grpc.swift @@ -130,8 +130,9 @@ extension Flipcash_Paymentrequest_V1_PaymentRequest { /// > /// > Anyone who can read the chat in full sees the request, its shares and /// > who has paid; someone who can only preview the chat sees the widget but - /// > not the request. No event announces a change to a request: clients - /// > refetch it to see payments and closes land. + /// > not the request. Changes to a request are announced best effort on the + /// > chat's event stream (see PaymentRequestUpdate); clients refetch a request + /// > to reconcile any they missed. /// > /// > In a group that is not private, a split's title and description are /// > plaintext and moderated, and the widget is sent as plain WidgetContent. @@ -328,8 +329,9 @@ extension Flipcash_Paymentrequest_V1_PaymentRequest { /// > /// > Anyone who can read the chat in full sees the request, its shares and /// > who has paid; someone who can only preview the chat sees the widget but - /// > not the request. No event announces a change to a request: clients - /// > refetch it to see payments and closes land. + /// > not the request. Changes to a request are announced best effort on the + /// > chat's event stream (see PaymentRequestUpdate); clients refetch a request + /// > to reconcile any they missed. /// > /// > In a group that is not private, a split's title and description are /// > plaintext and moderated, and the widget is sent as plain WidgetContent. diff --git a/Sources/Flipcash2ClientProtocol/push_v1_push_service.grpc.swift b/Sources/Flipcash2ClientProtocol/push_v1_push_service.grpc.swift index 17b5e25..f5dfa51 100644 --- a/Sources/Flipcash2ClientProtocol/push_v1_push_service.grpc.swift +++ b/Sources/Flipcash2ClientProtocol/push_v1_push_service.grpc.swift @@ -44,10 +44,23 @@ public enum Flipcash_Push_V1_Push { method: "DeleteTokens" ) } + /// Namespace for "SendTestPush" metadata. + public enum SendTestPush { + /// Request type for "SendTestPush". + public typealias Input = Flipcash_Push_V1_SendTestPushRequest + /// Response type for "SendTestPush". + public typealias Output = Flipcash_Push_V1_SendTestPushResponse + /// Descriptor for "SendTestPush". + public static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "flipcash.push.v1.Push"), + method: "SendTestPush" + ) + } /// Descriptors for all methods in the "flipcash.push.v1.Push" service. public static let descriptors: [GRPCCore.MethodDescriptor] = [ AddToken.descriptor, - DeleteTokens.descriptor + DeleteTokens.descriptor, + SendTestPush.descriptor ] } } @@ -112,6 +125,31 @@ extension Flipcash_Push_V1_Push { options: GRPCCore.CallOptions, onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable + + /// Call the "SendTestPush" method. + /// + /// > Source IDL Documentation: + /// > + /// > SendTestPush sends a fixed test notification to every device the caller + /// > has registered a push token for, so a user can check that notifications + /// > reach them. It carries no navigation and does not change the app badge. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Push_V1_SendTestPushRequest` message. + /// - serializer: A serializer for `Flipcash_Push_V1_SendTestPushRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Push_V1_SendTestPushResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sendTestPush( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } /// Generated client for the "flipcash.push.v1.Push" service. @@ -197,6 +235,42 @@ extension Flipcash_Push_V1_Push { onResponse: handleResponse ) } + + /// Call the "SendTestPush" method. + /// + /// > Source IDL Documentation: + /// > + /// > SendTestPush sends a fixed test notification to every device the caller + /// > has registered a push token for, so a user can check that notifications + /// > reach them. It carries no navigation and does not change the app badge. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Push_V1_SendTestPushRequest` message. + /// - serializer: A serializer for `Flipcash_Push_V1_SendTestPushRequest` messages. + /// - deserializer: A deserializer for `Flipcash_Push_V1_SendTestPushResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func sendTestPush( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Flipcash_Push_V1_Push.Method.SendTestPush.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } } } @@ -260,6 +334,37 @@ extension Flipcash_Push_V1_Push.ClientProtocol { onResponse: handleResponse ) } + + /// Call the "SendTestPush" method. + /// + /// > Source IDL Documentation: + /// > + /// > SendTestPush sends a fixed test notification to every device the caller + /// > has registered a push token for, so a user can check that notifications + /// > reach them. It carries no navigation and does not change the app badge. + /// + /// - Parameters: + /// - request: A request containing a single `Flipcash_Push_V1_SendTestPushRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func sendTestPush( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sendTestPush( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } } // Helpers providing sugared APIs for 'ClientProtocol' methods. @@ -330,4 +435,39 @@ extension Flipcash_Push_V1_Push.ClientProtocol { onResponse: handleResponse ) } + + /// Call the "SendTestPush" method. + /// + /// > Source IDL Documentation: + /// > + /// > SendTestPush sends a fixed test notification to every device the caller + /// > has registered a push token for, so a user can check that notifications + /// > reach them. It carries no navigation and does not change the app badge. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + public func sendTestPush( + _ message: Flipcash_Push_V1_SendTestPushRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sendTestPush( + request: request, + options: options, + onResponse: handleResponse + ) + } } \ No newline at end of file diff --git a/Sources/Flipcash2ClientProtocol/push_v1_push_service.pb.swift b/Sources/Flipcash2ClientProtocol/push_v1_push_service.pb.swift index 7451f24..7187167 100644 --- a/Sources/Flipcash2ClientProtocol/push_v1_push_service.pb.swift +++ b/Sources/Flipcash2ClientProtocol/push_v1_push_service.pb.swift @@ -174,6 +174,69 @@ public struct Flipcash_Push_V1_DeleteTokensResponse: Sendable { public init() {} } +public struct Flipcash_Push_V1_SendTestPushRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var auth: Flipcash_Common_V1_Auth { + get {return _auth ?? Flipcash_Common_V1_Auth()} + set {_auth = newValue} + } + /// Returns true if `auth` has been explicitly set. + public var hasAuth: Bool {return self._auth != nil} + /// Clears the value of `auth`. Subsequent reads from it will return its default value. + public mutating func clearAuth() {self._auth = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _auth: Flipcash_Common_V1_Auth? = nil +} + +public struct Flipcash_Push_V1_SendTestPushResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var result: Flipcash_Push_V1_SendTestPushResponse.Result = .ok + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum Result: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case ok // = 0 + case UNRECOGNIZED(Int) + + public init() { + self = .ok + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .ok: return 0 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Flipcash_Push_V1_SendTestPushResponse.Result] = [ + .ok, + ] + + } + + public init() {} +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "flipcash.push.v1" @@ -333,3 +396,71 @@ extension Flipcash_Push_V1_DeleteTokensResponse: SwiftProtobuf.Message, SwiftPro extension Flipcash_Push_V1_DeleteTokensResponse.Result: SwiftProtobuf._ProtoNameProviding { public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0") } + +extension Flipcash_Push_V1_SendTestPushRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SendTestPushRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}auth\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._auth) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._auth { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Push_V1_SendTestPushRequest, rhs: Flipcash_Push_V1_SendTestPushRequest) -> Bool { + if lhs._auth != rhs._auth {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Push_V1_SendTestPushResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SendTestPushResponse" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}result\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.result) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.result != .ok { + try visitor.visitSingularEnumField(value: self.result, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Flipcash_Push_V1_SendTestPushResponse, rhs: Flipcash_Push_V1_SendTestPushResponse) -> Bool { + if lhs.result != rhs.result {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Flipcash_Push_V1_SendTestPushResponse.Result: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0OK\0") +} diff --git a/flipcash2.lock b/flipcash2.lock index e12559e..748ea88 100644 --- a/flipcash2.lock +++ b/flipcash2.lock @@ -1,4 +1,4 @@ # Pinned upstream contract. Regenerate with scripts/sync-protos.sh . upstream: code-payments/flipcash2-protobuf-api -commit: b655df1770f1d588736ee3dcb712d9be5ca2cf41 -subject: Add anonymous reads for group invite flows (#159) +commit: 6de4f9adfd110a55b56fbfc99a5a466b3da789d1 +subject: Add SendTestPush RPC (#163) diff --git a/proto/activity/v1/model.proto b/proto/activity/v1/model.proto index 7941c77..6899ec5 100644 --- a/proto/activity/v1/model.proto +++ b/proto/activity/v1/model.proto @@ -8,6 +8,7 @@ option objc_class_prefix = "FCPBActivityV1"; import "common/v1/common.proto"; import "messaging/v1/model.proto"; +import "paymentrequest/v1/model.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -66,11 +67,21 @@ message DirectlySentCryptoNotificationMetadata { common.v1.UserId user_id = 2; } - // Set when the payment boosted a reaction (see BoostedReaction). A boost - // that paid one person names them in destination_identifier; one that - // paid both the message's sender and the group's creator names neither, - // and payment_amount is the whole boost. - BoostedReaction boosted_reaction = 3; + // What the payment was for, when it was made for something the feed can + // link to. Unset for any other payment (a DM or group payment, one handed + // off in person, ...). + oneof context { + // The payment boosted a reaction (see BoostedReaction). A boost that + // paid one person names them in destination_identifier; one that paid + // both the message's sender and the group's creator names neither, and + // payment_amount is the whole boost. + BoostedReaction boosted_reaction = 3; + + // The payment paid the payer's share of a payment request (see + // RequestedPayment). destination_identifier names the request's + // creator, and payment_amount is the share. + RequestedPayment requested_payment = 4; + } } message ReceivedCryptoNotificationMetadata { @@ -79,11 +90,21 @@ message ReceivedCryptoNotificationMetadata { common.v1.UserId user_id = 2; } - // Set when the payment was the recipient's share of a boost of a - // reaction (see BoostedReaction), paid to them as the boosted message's - // sender or its group's creator. source_identifier names the payer, and - // payment_amount is the recipient's share alone. - BoostedReaction boosted_reaction = 3; + // What the payment was for, when it was made for something the feed can + // link to. Unset for any other payment (a DM or group payment, one handed + // off in person, ...). + oneof context { + // The payment was the recipient's share of a boost of a reaction (see + // BoostedReaction), paid to them as the boosted message's sender or its + // group's creator. source_identifier names the payer, and + // payment_amount is the recipient's share alone. + BoostedReaction boosted_reaction = 3; + + // The payment was a debtor's share of the recipient's payment request + // (see RequestedPayment). source_identifier names the debtor, and + // payment_amount is the share. + RequestedPayment requested_payment = 4; + } } // The reaction a payment boosted: the payer's reaction with emoji on a @@ -102,6 +123,19 @@ message BoostedReaction { messaging.v1.Emoji emoji = 3 [(validate.rules).message.required = true]; } +// The payment request a payment paid a share of: a debtor's share, paid to +// the request's creator (see intent.v1.ChatMetadata.RequestedPayment). It is +// what the payment was for when it was made and is not kept up to date: the +// request may since have been closed or expired, and the viewer may no longer +// be a member of its group, in which case fetching the request is denied. +message RequestedPayment { + // The group chat the payment request is in. + common.v1.ChatId chat_id = 1 [(validate.rules).message.required = true]; + + // The payment request whose share was paid. + paymentrequest.v1.PaymentRequestId payment_request_id = 2 [(validate.rules).message.required = true]; +} + message WithdrewCryptoNotificationMetadata { // Deprecated in favour of swap_metadata SwapState swap_state = 1 [(validate.rules).enum.not_in = 0]; diff --git a/proto/chat/v1/chat_service.proto b/proto/chat/v1/chat_service.proto index 4f6dccb..0aa95c9 100644 --- a/proto/chat/v1/chat_service.proto +++ b/proto/chat/v1/chat_service.proto @@ -250,11 +250,21 @@ service Chat { // already has is a no-op for that field, and a request that sets nothing // is a no-op that returns OK. // + // A public group's rules may also be edited, within limits (see + // EditChatRequest.rules); a private group has none to edit. New rules + // apply from the edit on to every check the rules decide, but never + // remove anyone: a member who no longer satisfies the listener rules + // stays a member and keeps reading, as when their balance falls, though + // like then they cannot speak until they satisfy them again, and once + // they leave they must satisfy them to rejoin. The server may take a + // short while to apply new rules everywhere. + // // Every real change reaches the chat's members, including the caller's // other devices, on the event stream as one MetadataUpdate per field // changed: TitleChanged for the title, ProfilePictureChanged for the // profile picture, DescriptionChanged for the description, - // CoverPictureChanged for the cover picture. + // CoverPictureChanged for the cover picture, RulesChanged for the + // rules. rpc EditChat(EditChatRequest) returns (EditChatResponse); // MuteChat mutes a chat for the caller, until a time or indefinitely. @@ -1032,6 +1042,30 @@ message EditChatRequest { // The new cover picture. Left unchanged when unset. CoverPicture cover_picture = 5; + // The new rules, in full: every rule the chat is to carry, not only the + // ones that change, as Metadata.rules shows them. Left unchanged when + // unset. Only a public group created with a minimum listener balance + // can have its rules edited; a private group is DENIED, and any other + // group INVALID_RULES. + // + // The server currently accepts only the following changes from the + // group's current rules, and refuses anything else as INVALID_RULES: + // - The amount of the listener MinimumBalanceRequirement, which must + // stay set, in the same currency and the same mints. + // - Adding, removing, or changing the amount of a speaker + // MinimumBalanceRequirement. If set, it must be in the listener + // MinimumBalanceRequirement's currency and mints, and its amount must + // be larger by at least the currency's minimum transfer value. + // Every other rule (e.g. a StaffRequirement or a CreatorRequirement) + // must be included exactly as the group carries it, in any order. Every + // MinimumBalanceRequirement must be at least the currency's minimum + // transfer value (one unit at its last decimal place, e.g. 0.01 USD or + // 1 JPY), as at creation (see StartChatRequest.GroupChatParameters). + // The caller must satisfy the new rules, speaker rules included + // (RULES_NOT_SATISFIED otherwise). Rules equal to the group's current + // ones are a no-op. + Rules rules = 6; + message Title { string value = 1 [(validate.rules).string = { min_len: 1 @@ -1073,6 +1107,8 @@ message EditChatResponse { PROFILE_PICTURE_BLOB_NOT_ACCEPTED = 4; COVER_PICTURE_BLOB_NOT_ACCEPTED = 5; DESCRIPTION_MODERATED = 6; + INVALID_RULES = 7; + RULES_NOT_SATISFIED = 8; } // The metadata for the chat after the edit, as seen by the caller, diff --git a/proto/chat/v1/model.proto b/proto/chat/v1/model.proto index 07c0ace..ac8e037 100644 --- a/proto/chat/v1/model.proto +++ b/proto/chat/v1/model.proto @@ -79,7 +79,8 @@ message Metadata { RosterSummary roster_summary = 10 [(validate.rules).message.required = true]; // Rules governing participation in this chat. Only supported for group - // chats. If not set, the chat has no participation requirements. Never + // chats. If not set, the chat has no participation requirements. A + // public group's minimum balances may change (see Chat.EditChat). Never // set for a private group (see is_private), which admits a user by its // creator's approval or an invite instead. Rules rules = 11; @@ -397,6 +398,7 @@ message MetadataUpdate { ProfilePictureChanged profile_picture_changed = 5; DescriptionChanged description_changed = 6; CoverPictureChanged cover_picture_changed = 7; + RulesChanged rules_changed = 8; } // Refreshes the entire chat metadata @@ -457,6 +459,15 @@ message MetadataUpdate { // replacing Metadata.cover_picture. blob.v1.Media new_cover_picture = 1 [(validate.rules).message.required = true]; } + + // The chat's rules have changed (e.g. its minimum balances, via + // Chat.EditChat). Delivered to the chat's members, including the editor's + // other devices. Best-effort and applied as received; a client that + // suspects a miss refetches the chat via Chat.GetChat. + message RulesChanged { + // The chat's rules in full, replacing Metadata.rules. + Rules new_rules = 1 [(validate.rules).message.required = true]; + } } // RosterUpdate is a best-effort, real-time change to a chat's roster — a diff --git a/proto/event/v1/model.proto b/proto/event/v1/model.proto index 0ca317c..455cbc7 100644 --- a/proto/event/v1/model.proto +++ b/proto/event/v1/model.proto @@ -10,6 +10,7 @@ import "blob/v1/model.proto"; import "chat/v1/model.proto"; import "common/v1/common.proto"; import "messaging/v1/model.proto"; +import "paymentrequest/v1/model.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; import "validate/validate.proto"; @@ -147,6 +148,16 @@ message ChatUpdate { // event log; the creator reconciles any misses by refetching the lobby // with Chat.GetLobbyMembers. chat.v1.LobbyUpdateBatch lobby_updates = 9; + + // If present, best-effort real-time changes to the chat's payment + // requests: one created, carried whole, and each change after that (a + // share paid, a request completed or closed), carried as what it did. + // Sent to those who can read the chat in full, never to a redacted chat + // preview. Like reaction_updates, they are NOT part of the gap-detected + // event log; clients apply each to their copy by the request's version + // and reconcile any misses by refetching the request (see + // paymentrequest.v1.PaymentRequestUpdate). + paymentrequest.v1.PaymentRequestUpdateBatch payment_request_updates = 10; } // BlobUpdate notifies the recipient in real time that blobs they uploaded have diff --git a/proto/paymentrequest/v1/model.proto b/proto/paymentrequest/v1/model.proto index 1294644..bd6f9f7 100644 --- a/proto/paymentrequest/v1/model.proto +++ b/proto/paymentrequest/v1/model.proto @@ -50,9 +50,11 @@ message IdempotencyKey { // EXPIRED over OPEN, and judges an OPEN request past its ends_at as EXPIRED // itself. // -// Nothing announces a change in real time: no event carries a payment request -// today. Clients refetch a request to see payments and closes land, for -// example when they show its widget. +// Changes are announced in real time, best effort, by a PaymentRequestUpdate +// on the chat's event stream (event.v1.ChatUpdate.payment_request_updates): +// the request whole when it is created, and each change after that as the +// changes it made at its version. Clients refetch a request to reconcile any +// update they missed, for example when they show its widget. // // Requests are layered. The first layer is where the request lives: today a // group chat (GroupChatPaymentRequest). Within it, the second layer is the @@ -318,3 +320,104 @@ enum PaymentRequestStatus { PAYMENT_REQUEST_STATUS_EXPIRED = 4; } +// PaymentRequestUpdate is a best-effort, real-time change to a payment +// request, carried on its chat's event stream (see +// event.v1.ChatUpdate.payment_request_updates). It is sent when a request is +// created, carrying it whole (`created`), and on every change that advances +// its version, carrying what that change did (`updates`): a share paid, by +// payment or by hand, or a hand mark replaced by a payment; the last share +// paid, which also completes the request; and a close. Expiry is not +// announced: it comes from the clock and moves no version, so clients judge +// it from ends_at (see PAYMENT_REQUEST_STATUS_EXPIRED). +// +// Updates may arrive out of order, more than once, or not at all. A client +// applies them to its copy of the request by version: +// - `created`: like a fetched copy, keeping the one with the greater +// version (at an equal version, EXPIRED over OPEN). +// - `updates` at version V, to a copy at version L: +// - V <= L: already applied; ignore it. +// - V == L + 1: apply every Update, in order, then set version to V. +// - V > L + 1: an update was missed; refetch the request. +// A client holding no copy ignores it and fetches the request when it +// shows it. A client that meets an Update it cannot apply (a kind it does +// not know, or a share its copy does not have) applies none of them and +// refetches, rather than taking version V for a copy missing a change. +// +// A missed update is reconciled by refetching the request. A share paid by a +// payment is also announced by the payment's messaging.v1.CashContent +// message, which carries the request's payment_request_id and is part of the +// chat's gap-detected event log, so a client that sees one for a share it +// holds unpaid can refetch the request. +// +// Like GetPaymentRequest, an update reaches only those who can read the chat +// in full. It never carries the caller-specific payment_intent_id of +// GetPaymentRequestResponse; a debtor fetches the request to pay their share. +message PaymentRequestUpdate { + PaymentRequestId payment_request_id = 1 [(validate.rules).message.required = true]; + + oneof kind { + option (validate.required) = true; + + // The request was created. It is carried whole, as GetPaymentRequest + // returns it, and is applied like a fetched copy. + PaymentRequestMetadata created = 10; + + // One change to the request: everything it did, at one version. + Updates updates = 11; + } + + // Updates is everything one change to the request did, at the version + // it produced. + message Updates { + // The request's version after these updates. It advances by exactly + // one per change, so a client holding version - 1 applies them, and + // one holding less has missed one. + uint64 version = 1 [(validate.rules).uint64.gte = 2]; + + // What the change did, applied in order, all or nothing. A + // StatusUpdate is at most one, and the last. + repeated Update updates = 2 [(validate.rules).repeated = { + min_items: 1 + max_items: 100 + }]; + } + + message Update { + oneof kind { + option (validate.required) = true; + + SharePaymentUpdate share_payment_update = 1; + StatusUpdate status_update = 2; + } + } + + // SharePaymentUpdate is a share paid, by payment or by hand, or a hand + // mark replaced by a payment (see PaidShare). Clients replace the share + // with the same user_id by this one. + message SharePaymentUpdate { + // The share as it now stands. Its paid is always set. + Share share = 1 [(validate.rules).message.required = true]; + } + + // StatusUpdate is the request leaving OPEN: CLOSED by its creator, or + // COMPLETED by the payment of its last unpaid share, which comes in the + // same Updates, before it. Both are final, so a request's status changes + // at most once. Expiry is never announced (see above). + message StatusUpdate { + PaymentRequestStatus status = 1 [(validate.rules).enum = { + in: [2, 3] // COMPLETED, CLOSED + }]; + + // When the request stopped being OPEN (see + // PaymentRequestMetadata.closed_at). + google.protobuf.Timestamp closed_at = 2 [(validate.rules).timestamp.required = true]; + } +} + +message PaymentRequestUpdateBatch { + repeated PaymentRequestUpdate payment_request_updates = 1 [(validate.rules).repeated = { + min_items: 1 + max_items: 100 + }]; +} + diff --git a/proto/paymentrequest/v1/paymentrequest_service.proto b/proto/paymentrequest/v1/paymentrequest_service.proto index 1525368..a051445 100644 --- a/proto/paymentrequest/v1/paymentrequest_service.proto +++ b/proto/paymentrequest/v1/paymentrequest_service.proto @@ -7,7 +7,6 @@ option java_package = "com.codeinc.flipcash.gen.paymentrequest.v1"; option objc_class_prefix = "FPBPaymentRequestV1"; import "common/v1/common.proto"; -import "google/protobuf/timestamp.proto"; import "moderation/v1/model.proto"; import "paymentrequest/v1/model.proto"; import "validate/validate.proto"; @@ -34,8 +33,9 @@ import "validate/validate.proto"; // // Anyone who can read the chat in full sees the request, its shares and // who has paid; someone who can only preview the chat sees the widget but -// not the request. No event announces a change to a request: clients -// refetch it to see payments and closes land. +// not the request. Changes to a request are announced best effort on the +// chat's event stream (see PaymentRequestUpdate); clients refetch a request +// to reconcile any they missed. // // In a group that is not private, a split's title and description are // plaintext and moderated, and the widget is sent as plain WidgetContent. diff --git a/proto/push/v1/push_service.proto b/proto/push/v1/push_service.proto index fa1b959..795631c 100644 --- a/proto/push/v1/push_service.proto +++ b/proto/push/v1/push_service.proto @@ -16,6 +16,11 @@ service Push { // DeleteTokens removes all push tokens within an app install for a user rpc DeleteTokens(DeleteTokensRequest) returns (DeleteTokensResponse); + + // SendTestPush sends a fixed test notification to every device the caller + // has registered a push token for, so a user can check that notifications + // reach them. It carries no navigation and does not change the app badge. + rpc SendTestPush(SendTestPushRequest) returns (SendTestPushResponse); } message AddTokenRequest { @@ -51,3 +56,14 @@ message DeleteTokensResponse { OK = 0; } } + +message SendTestPushRequest { + common.v1.Auth auth = 1 [(validate.rules).message.required = true]; +} + +message SendTestPushResponse { + Result result = 1; + enum Result { + OK = 0; + } +}