diff --git a/.eslintrc.js b/.eslintrc.js index 8e39d62ac..c4b0b9ab3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,9 @@ module.exports = { root: true, - extends: ["universe/native", "universe/web"], - ignorePatterns: ["build"], -}; + extends: ['universe/native', 'universe/web'], + ignorePatterns: ['build'], + plugins: ['prettier'], + globals: { + __dirname: true, + }, +} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..28d549458 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,13 @@ +name: Lint +on: + pull_request: +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + - run: npm install + - run: npm run eslint diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 000000000..fec61a2a6 --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + semi: false, + singleQuote: true, + trailingComma: 'es5', + arrowParens: 'always', + printWidth: 80, +} diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 000000000..711cc6721 --- /dev/null +++ b/.swiftformat @@ -0,0 +1 @@ +--indent tab diff --git a/README.md b/README.md index a44997acd..49f865663 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Your app must use Android `minSdkVersion = 22` to work with the `xmtp-react-nati The [XMTP message API](https://xmtp.org/docs/concepts/architectural-overview#network-layer) revolves around a network client that allows retrieving and sending messages to other network participants. A client must be connected to a wallet on startup. If this is the very first time the client is created, the client will generate a [key bundle](https://xmtp.org/docs/concepts/key-generation-and-usage) that is used to [encrypt and authenticate messages](https://xmtp.org/docs/concepts/invitation-and-message-encryption). The key bundle persists encrypted in the network using a [wallet signature](https://xmtp.org/docs/concepts/account-signatures). The public side of the key bundle is also regularly advertised on the network to allow parties to establish shared encryption keys. All this happens transparently, without requiring any additional code. ```tsx -import { Client } from '@xmtp/xmtp-react-native' +import { Client } from '@xmtp/react-native-sdk' import { ConnectWallet, useSigner } from "@thirdweb-dev/react-native"; // Create the client with your wallet. This will connect to the XMTP development network by default @@ -105,7 +105,7 @@ A client is created with `Client.create(wallet: Signer): Promise` that r > The client connects to the XMTP `dev` environment by default. [Use `ClientOptions`](#configure-the-client) to change this and other parameters of the network connection. ```tsx -import { Client } from '@xmtp/xmtp-react-native' +import { Client } from '@xmtp/react-native-sdk' // Create the client with a `Signer` from your application const xmtp = await Client.create(wallet) ``` @@ -118,13 +118,14 @@ The client's network connection and key storage method can be configured with th | ------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | appVersion | `undefined` | Add a client app version identifier that's included with API requests.
For example, you can use the following format: `appVersion: APP_NAME + '/' + APP_VERSION`.
Setting this value provides telemetry that shows which apps are using the XMTP client SDK. This information can help XMTP developers provide app support, especially around communicating important SDK updates, including deprecations and required upgrades. | | env | `dev` | Connect to the specified XMTP network environment. Valid values include `dev`, `production`, or `local`. For important details about working with these environments, see [XMTP `production` and `dev` network environments](#xmtp-production-and-dev-network-environments). | +| codecs | `[new XMTP.ReactionCodec()]` | Add codecs to support additional content types. | ## Handle conversations Most of the time, when interacting with the network, you'll want to do it through `conversations`. Conversations are between two wallets. ```tsx -import { Client } from '@xmtp/xmtp-react-native' +import { Client } from '@xmtp/react-native-sdk' // Create the client with a `Signer` from your application const xmtp = await Client.create(wallet) const conversations = xmtp.conversations @@ -272,7 +273,7 @@ To learn more, see [Request and respect user consent](https://xmtp.org/docs/buil If you would like to check and see if a blockchain address is registered on the network before instantiating a client instance, you can use `Client.canMessage`. ```tsx -import { Client } from '@xmtp/xmtp-react-native' +import { Client } from '@xmtp/react-native-sdk' const isOnDevNetwork = await Client.canMessage( '0x3F11b27F323b62B159D2642964fa27C46C841897' @@ -292,7 +293,7 @@ For example: ```tsx const ethers = require('ethers') -const { Client } = require('@xmtp/xmtp-react-native') +const { Client } = require('@xmtp/react-native-sdk') async function main() { //Create a random wallet for example purposes. On the frontend you should replace it with the user's wallet (metamask, rainbow, etc) @@ -327,23 +328,40 @@ All send functions support `SendOptions` as an optional parameter. The `contentT To learn more about content types, see [Content types with XMTP](https://xmtp.org/docs/concepts/content-types). -The SDK preregisters the following codecs: +Support for other types of content can be added by registering additional `ContentCodecs` with the `Client`. Every codec is associated with a content type identifier, `ContentTypeId`, which is used to signal to the client which codec should be used to process the content that is being sent or received. +For example, see the [Native Codecs](https://github.com/xmtp/xmtp-react-native/tree/main/src/lib/NativeCodecs) available in `xmtp-react-native`. -- For [Android](https://github.com/xmtp/xmtp-react-native/blob/main/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt#L43-L53), using these [source codecs](https://github.com/xmtp/xmtp-android/tree/main/library/src/main/java/org/xmtp/android/library/codecs). +```ts +// Assuming we've loaded a fictional NumberCodec that can be used to encode numbers, +// and is identified with ContentTypeNumber, we can use it as follows. -- For [iOS](https://github.com/xmtp/xmtp-react-native/blob/main/ios/Wrappers/DecodedMessageWrapper.swift#L35-L48), using these [source codecs](https://github.com/xmtp/xmtp-ios/tree/main/Sources/XMTP/Codecs). +xmtp.register(new NumberCodec()) +conversation.send(3.14, { + contentType: ContentTypeNumber +}) +``` -```tsx - await conversation.send({ - reaction: { - reference: otherMessage.id, - action: "added", - schema: "unicode", - content: "đź’–", - }, - }); +Additional codecs can be configured through the `ClientOptions` parameter of `Client.create`. The `codecs` option is a list of codec instances that should be added to the default set of codecs (currently only the `TextCodec`). If a codec is added for a content type that is already in the default set, it will replace the original codec. + +```ts +// Adding support for `xmtp.org/reaction` content type +import { ReactionCodec } from '@xmtp/react-native-sdk' +const xmtp = Client.create(wallet, { codecs: [new ReactionCodec()] }) + +await conversation.send({ + reaction: { + reference: otherMessage.id, + action: "added", + schema: "unicode", + content: "đź’–", + }, +}); ``` +To learn more about how to build a custom content type, see [Build a custom content type](https://xmtp.org/docs/content-types/introduction#create-custom-content-types). + +Custom codecs and content types may be proposed as interoperable standards through XRCs. To learn about the custom content type proposal process, see [XIP-5](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-5-message-content-types.md). + ## Manually handle private key storage The SDK will handle key storage for the user by encrypting the private key bundle using a signature generated from the wallet, and storing the encrypted payload on the XMTP network. This can be awkward for some server-side applications, where you may only want to give the application access to the XMTP keys but not your wallet keys. Mobile applications may also want to store keys in a secure enclave rather than rely on decrypting the remote keys on the network each time the application starts up. @@ -351,7 +369,7 @@ The SDK will handle key storage for the user by encrypting the private key bundl You can export the unencrypted key bundle using the static method `Client.exportKeyBundle`, save it somewhere secure, and then provide those keys at a later time to initialize a new client using the exported XMTP identity. ```js -import { Client } from '@xmtp/xmtp-react-native' +import { Client } from '@xmtp/react-native-sdk' // Get the keys using a valid Signer. Save them somewhere secure. const keys = await Client.exportKeyBundle() // Create a client using keys returned from getKeys diff --git a/android/build.gradle b/android/build.gradle index a91e180c9..893fb66d0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -95,7 +95,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.6.16" + implementation "org.xmtp:android:0.6.17" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index cf0514ad7..9d4e4e6fd 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -278,6 +278,27 @@ class XMTPModule : Module() { ).toJson() } + AsyncFunction("sendEncodedContent") { clientAddress: String, topic: String, encodedContentData: List -> + val conversation = + findConversation( + clientAddress = clientAddress, + topic = topic + ) ?: throw XMTPException("no conversation found for $topic") + + val encodedContentDataBytes = + encodedContentData.foldIndexed(ByteArray(encodedContentData.size)) { i, a, v -> + a.apply { + set( + i, + v.toByte() + ) + } + } + val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes) + + conversation.send(encodedContent = encodedContent) + } + AsyncFunction("listConversations") { clientAddress: String -> logV("listConversations") val client = clients[clientAddress] ?: throw XMTPException("No client") @@ -298,7 +319,7 @@ class XMTPModule : Module() { val beforeDate = if (before != null) Date(before) else null val afterDate = if (after != null) Date(after) else null - conversation.messages( + conversation.decryptedMessages( limit = limit, before = beforeDate, after = afterDate, @@ -350,7 +371,7 @@ class XMTPModule : Module() { topicsList.add(Pair(topic, page)) } - client.conversations.listBatchMessages(topicsList) + client.conversations.listBatchDecryptedMessages(topicsList) .map { DecodedMessageWrapper.encode(it) } } @@ -496,7 +517,7 @@ class XMTPModule : Module() { topic = topic ) ?: throw XMTPException("no conversation found for $topic") - val decodedMessage = conversation.decode(envelope) + val decodedMessage = conversation.decrypt(envelope) DecodedMessageWrapper.encode(decodedMessage) } @@ -591,7 +612,7 @@ class XMTPModule : Module() { subscriptions[getMessagesKey(clientAddress)]?.cancel() subscriptions[getMessagesKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { try { - client.conversations.streamAllMessages().collect { message -> + client.conversations.streamAllDecryptedMessages().collect { message -> sendEvent( "message", mapOf( @@ -617,7 +638,7 @@ class XMTPModule : Module() { subscriptions[conversation.cacheKey(clientAddress)] = CoroutineScope(Dispatchers.IO).launch { try { - conversation.streamMessages().collect { message -> + conversation.streamDecryptedMessages().collect { message -> sendEvent( "message", mapOf( diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt index 775891a85..0521e2e8b 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt @@ -1,6 +1,7 @@ package expo.modules.xmtpreactnativesdk.wrappers import android.util.Base64 +import com.google.gson.GsonBuilder import com.google.gson.JsonObject import com.google.gson.JsonParser import com.google.protobuf.ByteString @@ -14,6 +15,7 @@ import org.xmtp.android.library.codecs.ContentTypeReadReceipt import org.xmtp.android.library.codecs.ContentTypeRemoteAttachment import org.xmtp.android.library.codecs.ContentTypeReply import org.xmtp.android.library.codecs.ContentTypeText +import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReadReceipt @@ -28,16 +30,17 @@ import org.xmtp.android.library.codecs.description import org.xmtp.android.library.codecs.getReactionAction import org.xmtp.android.library.codecs.getReactionSchema import org.xmtp.android.library.codecs.id -import org.xmtp.proto.message.contents.Content.EncodedContent import java.net.URL class ContentJson( val type: ContentTypeId, val content: Any?, + private val encodedContent: EncodedContent? = null, ) { constructor(encoded: EncodedContent) : this( type = encoded.type, content = encoded.decoded(), + encodedContent = encoded ); companion object { @@ -157,7 +160,12 @@ class ContentJson( ContentTypeReply.id -> mapOf( "reply" to mapOf( "reference" to (content as Reply).reference, - "content" to ContentJson(content.contentType, content.content).toJsonMap(), + "content" to ContentJson( + content.contentType, + content.content, + encodedContent + ).toJsonMap(), + "contentType" to content.contentType.description ) ) @@ -165,11 +173,31 @@ class ContentJson( "readReceipt" to "" ) - else -> mapOf( - "unknown" to mapOf( - "contentTypeId" to type.description - ) - ) + else -> { + val json = JsonObject() + encodedContent?.let { + val typeJson = JsonObject() + typeJson.addProperty("authorityId", encodedContent.type.authorityId) + typeJson.addProperty("typeId", encodedContent.type.typeId) + typeJson.addProperty("versionMajor", encodedContent.type.versionMajor) + typeJson.addProperty("versionMinor", encodedContent.type.versionMinor) + val parameters = GsonBuilder().create().toJson(encodedContent.parametersMap) + + json.addProperty("fallback", encodedContent.fallback) + json.add("parameters", JsonParser.parseString(parameters)) + json.add("type", typeJson) + } + val encodedContentJSON = json.toString() + if (encodedContentJSON.isNotBlank()) { + mapOf("encoded" to encodedContentJSON) + } else { + mapOf( + "unknown" to mapOf( + "contentTypeId" to type.description + ) + ) + } + } } } -} +} \ No newline at end of file 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 a0f7a3126..775ef66a5 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt @@ -1,27 +1,26 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder -import org.xmtp.android.library.DecodedMessage import org.xmtp.android.library.codecs.description -import org.xmtp.android.library.codecs.id +import org.xmtp.android.library.messages.DecryptedMessage class DecodedMessageWrapper { companion object { - fun encode(model: DecodedMessage): String { + fun encode(model: DecryptedMessage): String { val gson = GsonBuilder().create() val message = encodeMap(model) return gson.toJson(message) } - fun encodeMap(model: DecodedMessage): Map = mapOf( + 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.sent.time, - "fallback" to model.fallbackContent + "sent" to model.sentAt.time, + "fallback" to model.encodedContent.fallback ) } } diff --git a/example/App.tsx b/example/App.tsx index 75b6a10e3..d281fe4b0 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,17 +1,17 @@ -import React from "react"; +import { NavigationContainer } from '@react-navigation/native' +import React from 'react' +import { Button } from 'react-native' +import { QueryClient, QueryClientProvider } from 'react-query' -import LaunchScreen from "./src/LaunchScreen"; -import TestScreen from "./src/TestScreen"; -import HomeScreen from "./src/HomeScreen"; -import ConversationScreen from "./src/ConversationScreen"; -import ConversationCreateScreen from "./src/ConversationCreateScreen"; -import { NavigationContainer } from "@react-navigation/native"; -import { XmtpContextProvider } from "./src/XmtpContext"; -import { Navigator } from "./src/Navigation"; -import { QueryClient, QueryClientProvider } from "react-query"; -import { Button } from "react-native"; +import ConversationCreateScreen from './src/ConversationCreateScreen' +import ConversationScreen from './src/ConversationScreen' +import HomeScreen from './src/HomeScreen' +import LaunchScreen from './src/LaunchScreen' +import { Navigator } from './src/Navigation' +import TestScreen from './src/TestScreen' +import { XmtpContextProvider } from './src/XmtpContext' -const queryClient = new QueryClient(); +const queryClient = new QueryClient() export default function App() { return ( @@ -22,36 +22,36 @@ export default function App() { name="launch" component={LaunchScreen} options={{ - title: "XMTP RN Example", + title: 'XMTP RN Example', headerStyle: { - backgroundColor: "rgb(49 0 110)", + backgroundColor: 'rgb(49 0 110)', }, - headerTintColor: "#fff", + headerTintColor: '#fff', headerTitleStyle: { - fontWeight: "bold", + fontWeight: 'bold', }, }} /> ({ - title: "My Conversations", + title: 'My Conversations', headerStyle: { - backgroundColor: "rgb(49 0 110)", + backgroundColor: 'rgb(49 0 110)', }, - headerTintColor: "#fff", + headerTintColor: '#fff', headerTitleStyle: { - fontWeight: "bold", + fontWeight: 'bold', }, headerRight: () => (