diff --git a/example/README.md b/example/README.md index 629e5fa89..802300eb4 100644 --- a/example/README.md +++ b/example/README.md @@ -15,6 +15,24 @@ npx pod-install yarn run [ios or android] ``` +### If testing the remote attachment content type, run (from example directory): + +```bash +yarn run upload:up +``` + +If running on andoid also run: + +```bash +adb reverse tcp:8443 tcp:8443 +``` + +To clean up after testing you can run: + +```bash +yarn run upload:down +``` + ### Configure ThirdWeb Client API > Note - The connect wallet button will still work without adding a client id, you just may see some extra network errors when viewing account info in the Thirdweb button after connecting. @@ -38,19 +56,21 @@ Running tests locally is useful when updating GitHub actions, or locally testing 1. [Install Docker](https://docs.docker.com/get-docker/) -2. Start a local XMTP server (from example directory) +2. Start a local XMTP server ```bash - docker-compose -p xmtp -f dev/local/docker-compose.yml up -d + git clone https://github.com/xmtp/libxmtp.git + cd libxmtp + dev/up ``` 3. Verify the XMTP server is running ```bash docker-compose ls NAME STATUS CONFIG FILES - xmtp running(3) /xmtp-react-native/example/dev/local/docker-compose.yml + libxmtp running(9) /libxmtp/dev/docker/docker-compose.yml ``` 4. You can now run unit tests on your local emulators 5. You can stop the XMTP server with the following command: ```bash - docker-compose -p xmtp -f dev/local/docker-compose.yml down + dev/down ``` diff --git a/example/dev/local/docker-compose.yml b/example/dev/local/docker-compose.yml index 0a09c8d48..b52cae67f 100644 --- a/example/dev/local/docker-compose.yml +++ b/example/dev/local/docker-compose.yml @@ -1,38 +1,5 @@ version: "3.8" services: - wakunode: - image: xmtp/node-go:latest - platform: linux/amd64 - environment: - - GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67 - command: - - --store.enable - - --store.db-connection-string=postgres://postgres:xmtp@db:5432/postgres?sslmode=disable - - --store.reader-db-connection-string=postgres://postgres:xmtp@db:5432/postgres?sslmode=disable - - --wait-for-db=30s - - --api.authn.enable - ports: - - 9001:9001 - - 5555:5555 # http message API - - 5556:5556 # grpc message API - depends_on: - - db - healthcheck: - test: [ "CMD", "lsof", "-i", ":5556" ] - interval: 3s - timeout: 10s - retries: 5 - db: - image: postgres:13 - environment: - POSTGRES_PASSWORD: xmtp - js: - restart: always - platform: linux/amd64 - depends_on: - wakunode: - condition: service_healthy - build: ./test upload-service: build: ./upload-service @@ -42,7 +9,7 @@ services: caddy: image: caddy:latest ports: - - "443:443" + - "8443:8443" volumes: - ./upload-service/Caddyfile:/etc/caddy/Caddyfile - ./upload-service/data/data:/data diff --git a/example/dev/local/upload-service/Caddyfile b/example/dev/local/upload-service/Caddyfile index 351a28346..b8aef9409 100644 --- a/example/dev/local/upload-service/Caddyfile +++ b/example/dev/local/upload-service/Caddyfile @@ -1,4 +1,4 @@ -localhost { +localhost:8443 { tls internal reverse_proxy upload-service:4567 } diff --git a/example/package.json b/example/package.json index 2ebe867ba..eedc8bf96 100644 --- a/example/package.json +++ b/example/package.json @@ -6,7 +6,9 @@ "android": "expo run:android", "ios": "expo run:ios", "ios:clean": "expo run:ios --no-build-cache", - "web": "expo start --web" + "web": "expo start --web", + "upload:up": "docker-compose -p xmtp -f dev/local/docker-compose.yml up -d", + "upload:down": "docker-compose -p xmtp -f dev/local/docker-compose.yml down" }, "dependencies": { "@react-native-async-storage/async-storage": "^1.21.0", diff --git a/example/src/LaunchScreen.tsx b/example/src/LaunchScreen.tsx index 4e7df0776..cd0703c76 100644 --- a/example/src/LaunchScreen.tsx +++ b/example/src/LaunchScreen.tsx @@ -2,7 +2,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import { ConnectWallet, useSigner } from '@thirdweb-dev/react-native' import React, { useCallback, useEffect, useState } from 'react' import { Button, ScrollView, StyleSheet, Text, View } from 'react-native' -import EncryptedStorage from 'react-native-encrypted-storage' import ModalSelector from 'react-native-modal-selector' import * as XMTP from 'xmtp-react-native-sdk' import { useXmtp } from 'xmtp-react-native-sdk' diff --git a/example/src/TestScreen.tsx b/example/src/TestScreen.tsx index f8eded841..c5eb979c6 100644 --- a/example/src/TestScreen.tsx +++ b/example/src/TestScreen.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react' import { View, Text, Button, ScrollView } from 'react-native' import { clientTests } from './tests/clientTests' +import { contentTypeTests } from './tests/contentTypeTests' import { conversationTests } from './tests/conversationTests' import { dmTests } from './tests/dmTests' import { groupPerformanceTests } from './tests/groupPerformanceTests' @@ -10,7 +11,6 @@ import { groupPermissionsTests } from './tests/groupPermissionsTests' import { groupTests } from './tests/groupTests' import { restartStreamTests } from './tests/restartStreamsTests' import { Test } from './tests/test-utils' - type Result = 'waiting' | 'running' | 'success' | 'failure' | 'error' function TestView({ @@ -114,6 +114,7 @@ export enum TestCategory { restartStreans = 'restartStreams', groupPermissions = 'groupPermissions', groupPerformance = 'groupPerformance', + contentType = 'contentType', } export default function TestScreen(): JSX.Element { @@ -129,6 +130,7 @@ export default function TestScreen(): JSX.Element { ...conversationTests, ...restartStreamTests, ...groupPermissionsTests, + ...contentTypeTests, ] let activeTests, title switch (params.testSelection) { @@ -164,6 +166,10 @@ export default function TestScreen(): JSX.Element { activeTests = groupPerformanceTests title = 'Group Performance Unit Tests' break + case TestCategory.contentType: + activeTests = contentTypeTests + title = 'Content Type Unit Tests' + break } return ( diff --git a/example/src/storage.ts b/example/src/storage.ts index 3801702b2..3b8a6acbf 100644 --- a/example/src/storage.ts +++ b/example/src/storage.ts @@ -7,7 +7,7 @@ import ReactNativeBlobUtil from 'react-native-blob-util' const useLocalServer = !process.env.REACT_APP_USE_LOCAL_SERVER const storageUrl = useLocalServer - ? 'https://localhost' + ? 'https://localhost:8443' : process.env.REACT_APP_STORAGE_URL const headers = { 'Content-Type': 'application/octet-stream', @@ -19,15 +19,26 @@ export async function uploadFile( ): Promise { const url = `${storageUrl}/${fileId}` console.log('uploading to', url) - await ReactNativeBlobUtil.config({ - fileCache: true, - trusty: useLocalServer, - }).fetch( - 'POST', - url, - headers, - ReactNativeBlobUtil.wrap(localFileUri.slice('file://'.length)) - ) + + try { + await ReactNativeBlobUtil.config({ + fileCache: true, + trusty: useLocalServer, + }).fetch( + 'POST', + url, + headers, + ReactNativeBlobUtil.wrap(localFileUri.slice('file://'.length)) + ) + } catch (error) { + console.error( + 'Error during file upload:', + error, + 'Did you run the `yarn run upload:up` command from the xmtp-react-native/example directory?', + 'Did you run `adb reverse tcp:8443 tcp:8443` if testing on Android?' + ) + throw error + } return url } diff --git a/example/src/tests/contentTypeTests.ts b/example/src/tests/contentTypeTests.ts new file mode 100644 index 000000000..0df03aa7d --- /dev/null +++ b/example/src/tests/contentTypeTests.ts @@ -0,0 +1,96 @@ +import ReactNativeBlobUtil from 'react-native-blob-util' + +import { Test, createClients, delayToPropogate } from './test-utils' +import { RemoteAttachmentContent } from '../../../src/index' +const { fs } = ReactNativeBlobUtil + +export const contentTypeTests: Test[] = [] +let counter = 1 +function test(name: string, perform: () => Promise) { + contentTypeTests.push({ + name: String(counter++) + '. ' + name, + run: perform, + }) +} + +test('remote attachments should work', async () => { + const [alix, bo] = await createClients(2) + const convo = await alix.conversations.newConversation(bo.address) + + // Alice is sending Bob a file from her phone. + const filename = `${Date.now()}.txt` + const file = `${fs.dirs.CacheDir}/${filename}` + await fs.writeFile(file, 'hello world', 'utf8') + const { encryptedLocalFileUri, metadata } = await alix.encryptAttachment({ + fileUri: `file://${file}`, + mimeType: 'text/plain', + }) + + const encryptedFile = encryptedLocalFileUri.slice('file://'.length) + const originalContent = await fs.readFile(file, 'base64') + const encryptedContent = await fs.readFile(encryptedFile, 'base64') + if (encryptedContent === originalContent) { + throw new Error('encrypted file should not match original') + } + + // This is where the app will upload the encrypted file to a remote server and generate a URL. + // let url = await uploadFile(encryptedLocalFileUri); + const url = 'https://example.com/123' + + // Together with the metadata, we send the URL as a remoteAttachment message to the conversation. + await convo.send({ + remoteAttachment: { + ...metadata, + scheme: 'https://', + url, + }, + }) + await delayToPropogate() + + // Now we should see the remote attachment message. + const messages = await convo.messages() + if (messages.length !== 1) { + throw new Error('Expected 1 message') + } + const message = messages[0] + + if (message.contentTypeId !== 'xmtp.org/remoteStaticAttachment:1.0') { + throw new Error('Expected correctly formatted typeId') + } + if (!message.content()) { + throw new Error('Expected remoteAttachment') + } + if ( + (message.content() as RemoteAttachmentContent).url !== + 'https://example.com/123' + ) { + throw new Error('Expected url to match') + } + + // This is where the app prompts the user to download the encrypted file from `url`. + // TODO: let downloadedFile = await downloadFile(url); + // But to simplify this test, we're just going to copy + // the previously encrypted file and pretend that we just downloaded it. + const downloadedFileUri = `file://${fs.dirs.CacheDir}/${Date.now()}.bin` + await fs.cp( + new URL(encryptedLocalFileUri).pathname, + new URL(downloadedFileUri).pathname + ) + + // Now we can decrypt the downloaded file using the message metadata. + const attached = await alix.decryptAttachment({ + encryptedLocalFileUri: downloadedFileUri, + metadata: message.content() as RemoteAttachmentContent, + }) + if (attached.mimeType !== 'text/plain') { + throw new Error('Expected mimeType to match') + } + if (attached.filename !== filename) { + throw new Error(`Expected ${attached.filename} to equal ${filename}`) + } + const text = await fs.readFile(new URL(attached.fileUri).pathname, 'utf8') + if (text !== 'hello world') { + throw new Error('Expected text to match') + } + return true +}) diff --git a/example/src/tests/test-utils.ts b/example/src/tests/test-utils.ts index d920f0aad..ce0f53f83 100644 --- a/example/src/tests/test-utils.ts +++ b/example/src/tests/test-utils.ts @@ -1,5 +1,10 @@ import { Platform } from 'expo-modules-core' -import { Client, GroupUpdatedCodec, Group } from 'xmtp-react-native-sdk' +import { + Client, + GroupUpdatedCodec, + Group, + RemoteAttachmentCodec, +} from 'xmtp-react-native-sdk' export type Test = { name: string @@ -34,6 +39,7 @@ export async function createClients(numClients: number): Promise { dbEncryptionKey: keyBytes, }) client.register(new GroupUpdatedCodec()) + client.register(new RemoteAttachmentCodec()) clients.push(client) } return clients