diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt index 775ef66a5..6b87624f7 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt @@ -13,14 +13,19 @@ class DecodedMessageWrapper { return gson.toJson(message) } - fun encodeMap(model: DecryptedMessage): Map = mapOf( - "id" to model.id, - "topic" to model.topic, - "contentTypeId" to model.encodedContent.type.description, - "content" to ContentJson(model.encodedContent).toJsonMap(), - "senderAddress" to model.senderAddress, - "sent" to model.sentAt.time, - "fallback" to model.encodedContent.fallback - ) + fun encodeMap(model: DecryptedMessage): Map { + // Kotlin/Java Protos don't support null values and will always put the default "" + // Check if there is a fallback, if there is then make it the set fallback, if not null + val fallback = if (model.encodedContent.hasFallback()) model.encodedContent.fallback else null + return mapOf( + "id" to model.id, + "topic" to model.topic, + "contentTypeId" to model.encodedContent.type.description, + "content" to ContentJson(model.encodedContent).toJsonMap(), + "senderAddress" to model.senderAddress, + "sent" to model.sentAt.time, + "fallback" to fallback + ) + } } } diff --git a/example/src/tests.ts b/example/src/tests.ts index 9a395214d..2204c699d 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -31,6 +31,20 @@ const ContentTypeNumber: ContentTypeId = { versionMinor: 0, } +const ContentTypeNumberWithUndefinedFallback: ContentTypeId = { + authorityId: 'org', + typeId: 'number_undefined_fallback', + versionMajor: 1, + versionMinor: 0, +} + +const ContentTypeNumberWithEmptyFallback: ContentTypeId = { + authorityId: 'org', + typeId: 'number_empty_fallback', + versionMajor: 1, + versionMinor: 0, +} + export type NumberRef = { topNumber: { bottomNumber: number @@ -66,6 +80,20 @@ class NumberCodec implements JSContentCodec { } } +class NumberCodecUndefinedFallback extends NumberCodec { + contentType = ContentTypeNumberWithUndefinedFallback + fallback(content: NumberRef): string | undefined { + return undefined + } +} + +class NumberCodecEmptyFallback extends NumberCodec { + contentType = ContentTypeNumberWithEmptyFallback + fallback(content: NumberRef): string | undefined { + return '' + } +} + const LONG_STREAM_DELAY = 20000 export type Test = { @@ -1184,6 +1212,62 @@ test('correctly handles lowercase addresses', async () => { return true }) +test('handle fallback types appropriately', async () => { + const bob = await Client.createRandom({ + env: 'local', + codecs: [ + new NumberCodecEmptyFallback(), + new NumberCodecUndefinedFallback(), + ], + }) + const alice = await Client.createRandom({ + env: 'local', + }) + bob.register(new NumberCodecEmptyFallback()) + bob.register(new NumberCodecUndefinedFallback()) + const bobConvo = await bob.conversations.newConversation(alice.address) + const aliceConvo = await alice.conversations.newConversation(bob.address) + + await bobConvo.send(12, { contentType: ContentTypeNumberWithEmptyFallback }) + + await bobConvo.send(12, { + contentType: ContentTypeNumberWithUndefinedFallback, + }) + + const messages = await aliceConvo.messages() + assert(messages.length === 2, 'did not get messages') + + const messageUndefinedFallback = messages[0] + const messageWithDefinedFallback = messages[1] + + let message1Content = undefined + try { + message1Content = messageUndefinedFallback.content() + } catch { + message1Content = messageUndefinedFallback.fallback + } + + assert( + message1Content === undefined, + 'did not get content properly when empty fallback: ' + + JSON.stringify(message1Content) + ) + + let message2Content = undefined + try { + message2Content = messageWithDefinedFallback.content() + } catch { + message2Content = messageWithDefinedFallback.fallback + } + + assert( + message2Content === '', + 'did not get content properly: ' + JSON.stringify(message2Content) + ) + + return true +}) + test('instantiate frames client correctly', async () => { const frameUrl = 'https://fc-polls-five.vercel.app/polls/01032f47-e976-42ee-9e3d-3aac1324f4b8' diff --git a/ios/Wrappers/DecodedMessageWrapper.swift b/ios/Wrappers/DecodedMessageWrapper.swift index 72d6c2368..b8f726fde 100644 --- a/ios/Wrappers/DecodedMessageWrapper.swift +++ b/ios/Wrappers/DecodedMessageWrapper.swift @@ -5,6 +5,9 @@ import XMTP // into react native. struct DecodedMessageWrapper { static func encodeToObj(_ model: XMTP.DecryptedMessage, client: Client) throws -> [String: Any] { + // Swift Protos don't support null values and will always put the default "" + // Check if there is a fallback, if there is then make it the set fallback, if not null + let fallback = model.encodedContent.hasFallback ? model.encodedContent.fallback : nil return [ "id": model.id, "topic": model.topic, @@ -12,7 +15,7 @@ struct DecodedMessageWrapper { "content": try ContentJson.fromEncoded(model.encodedContent, client: client).toJsonMap() as Any, "senderAddress": model.senderAddress, "sent": UInt64(model.sentAt.timeIntervalSince1970 * 1000), - "fallback": model.encodedContent.fallback, + "fallback": fallback, ] } diff --git a/src/lib/DecodedMessage.ts b/src/lib/DecodedMessage.ts index b5f7b23c2..ba9f5965f 100644 --- a/src/lib/DecodedMessage.ts +++ b/src/lib/DecodedMessage.ts @@ -1,13 +1,11 @@ +import { Buffer } from 'buffer' + import { Client } from './Client' import { - ContentCodec, JSContentCodec, NativeContentCodec, NativeMessageContent, } from './ContentCodec' -import { ReplyCodec } from './NativeCodecs/ReplyCodec' -import { TextCodec } from './NativeCodecs/TextCodec' -import { Buffer } from 'buffer' const allowEmptyProperties: (keyof NativeMessageContent)[] = [ 'text', @@ -81,7 +79,8 @@ export class DecodedMessage { this.senderAddress = senderAddress this.sent = sent this.nativeContent = content - this.fallback = fallback + // undefined comes back as null when bridged, ensure undefined so integrators don't have to add a new check for null as well + this.fallback = fallback ?? undefined } content(): ContentTypes {